From 928ae4ec2252da2ec2e01930c15c92274f9e7af9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 10:56:27 -0500 Subject: [PATCH 1/4] oauthController: calculate the script hash on the fly so we can edit it --- src/controllers/api/oauth.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index 4f1f495..e45b31a 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,3 +1,4 @@ +import { encodeBase64 } from '@std/encoding/base64'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; @@ -60,7 +61,7 @@ const createTokenController: AppController = async (c) => { }; /** Display the OAuth form. */ -const oauthController: AppController = (c) => { +const oauthController: AppController = async (c) => { const encodedUri = c.req.query('redirect_uri'); if (!encodedUri) { return c.text('Missing `redirect_uri` query param.', 422); @@ -68,17 +69,7 @@ const oauthController: AppController = (c) => { const redirectUri = maybeDecodeUri(encodedUri); - c.res.headers.set( - 'content-security-policy', - "default-src 'self' 'sha256-m2qD6rbE2Ixbo2Bjy2dgQebcotRIAawW7zbmXItIYAM='", - ); - - return c.html(` - - - Log in with Ditto - - + `; + + const hash = encodeBase64(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(script))); + + c.res.headers.set( + 'content-security-policy', + `default-src 'self' 'sha256-${hash}'`, + ); + + return c.html(` + + + Log in with Ditto + +
From bdfa6f882640006f9fd3a7a1e7dd9de76b4b5eb0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 12:32:40 -0500 Subject: [PATCH 2/4] Add a getInstanceMetadata function to DRY a few controllers --- src/controllers/api/instance.ts | 20 ++++++---------- src/controllers/nostr/relay-info.ts | 15 ++++-------- src/utils/instance.ts | 37 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 src/utils/instance.ts diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 70f38e1..cc71b1f 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,25 +1,19 @@ -import { NSchema as n } from '@nostrify/nostrify'; - -import { type AppController } from '@/app.ts'; +import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; -import { Storages } from '@/storages.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; - const { signal } = c.req.raw; - - const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); + const meta = await getInstanceMetadata(c.req.raw.signal); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; return c.json({ uri: host, - title: meta.name ?? 'Ditto', - description: meta.about ?? 'Nostr and the Fediverse', - short_description: meta.tagline ?? meta.about ?? 'Nostr and the Fediverse', + title: meta.name, + description: meta.about, + short_description: meta.tagline, registrations: true, max_toot_chars: Conf.postCharLimit, configuration: { @@ -59,7 +53,7 @@ const instanceController: AppController = async (c) => { streaming_api: `${wsProtocol}//${host}`, }, version: '0.0.0 (compatible; Ditto 0.0.1)', - email: meta.email ?? `postmaster@${host}`, + email: meta.email, nostr: { pubkey: Conf.pubkey, relay: `${wsProtocol}//${host}/relay`, diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index a56df51..192cab2 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,20 +1,15 @@ -import { NSchema as n } from '@nostrify/nostrify'; - import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; -import { Storages } from '@/storages.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; const relayInfoController: AppController = async (c) => { - const { signal } = c.req.raw; - const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); + const meta = await getInstanceMetadata(c.req.raw.signal); return c.json({ - name: meta.name ?? 'Ditto', - description: meta.about ?? 'Nostr and the Fediverse.', + name: meta.name, + description: meta.about, pubkey: Conf.pubkey, - contact: `mailto:${meta.email ?? `postmaster@${Conf.url.host}`}`, + contact: meta.email, supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], software: 'Ditto', version: '0.0.0', diff --git a/src/utils/instance.ts b/src/utils/instance.ts new file mode 100644 index 0000000..004e4cf --- /dev/null +++ b/src/utils/instance.ts @@ -0,0 +1,37 @@ +import { NostrEvent, NostrMetadata, NSchema as n } from '@nostrify/nostrify'; + +import { Conf } from '@/config.ts'; +import { serverMetaSchema } from '@/schemas/nostr.ts'; +import { Storages } from '@/storages.ts'; + +/** Like NostrMetadata, but some fields are required and also contains some extra fields. */ +export interface InstanceMetadata extends NostrMetadata { + name: string; + about: string; + tagline: string; + email: string; + event?: NostrEvent; +} + +/** Get and parse instance metadata from the kind 0 of the admin user. */ +export async function getInstanceMetadata(signal?: AbortSignal): Promise { + const [event] = await Storages.db.query( + [{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], + { signal }, + ); + + const meta = n + .json() + .pipe(serverMetaSchema) + .catch({}) + .parse(event?.content); + + return { + ...meta, + name: meta.name ?? 'Ditto', + about: meta.about ?? 'Nostr community server', + tagline: meta.tagline ?? meta.about ?? 'Nostr community server', + email: meta.email ?? `postmaster@${Conf.url.host}`, + event, + }; +} From 3b0739f187172decc27aded70756031cc454c2e6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 13:12:46 -0500 Subject: [PATCH 3/4] Add a getClientConnectUri function, add "Nostr Connect" link in the OAuth form --- src/controllers/api/oauth.ts | 6 +++++- src/utils/connect.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/utils/connect.ts diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index e45b31a..a755a4d 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -2,10 +2,11 @@ import { encodeBase64 } from '@std/encoding/base64'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { lodash } from '@/deps.ts'; import { AppController } from '@/app.ts'; +import { lodash } from '@/deps.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; +import { getClientConnectUri } from '@/utils/connect.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), @@ -68,6 +69,7 @@ const oauthController: AppController = async (c) => { } const redirectUri = maybeDecodeUri(encodedUri); + const connectUri = await getClientConnectUri(c.req.raw.signal); const script = ` window.addEventListener('load', function() { @@ -101,6 +103,8 @@ const oauthController: AppController = async (c) => { +
+ Nostr Connect `); diff --git a/src/utils/connect.ts b/src/utils/connect.ts new file mode 100644 index 0000000..0c69a52 --- /dev/null +++ b/src/utils/connect.ts @@ -0,0 +1,20 @@ +import { Conf } from '@/config.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; + +/** Get NIP-46 `nostrconnect://` URI for the Ditto server. */ +export async function getClientConnectUri(signal?: AbortSignal): Promise { + const uri = new URL('nostrconnect://'); + const { name, description } = await getInstanceMetadata(signal); + + const metadata = { + name, + description, + url: Conf.localDomain, + }; + + uri.host = Conf.pubkey; + uri.searchParams.set('relay', Conf.relay); + uri.searchParams.set('metadata', JSON.stringify(metadata)); + + return uri.toString(); +} From dc8010a78eb1fd54e5403649d0127188f586c625 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 13:26:26 -0500 Subject: [PATCH 4/4] getClientConnectUri: fix description value --- src/utils/connect.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/utils/connect.ts b/src/utils/connect.ts index 0c69a52..8b3fdf8 100644 --- a/src/utils/connect.ts +++ b/src/utils/connect.ts @@ -1,14 +1,21 @@ import { Conf } from '@/config.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; +/** NIP-46 client-connect metadata. */ +interface ConnectMetadata { + name: string; + description: string; + url: string; +} + /** Get NIP-46 `nostrconnect://` URI for the Ditto server. */ export async function getClientConnectUri(signal?: AbortSignal): Promise { const uri = new URL('nostrconnect://'); - const { name, description } = await getInstanceMetadata(signal); + const { name, tagline } = await getInstanceMetadata(signal); - const metadata = { + const metadata: ConnectMetadata = { name, - description, + description: tagline, url: Conf.localDomain, };