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/api/oauth.ts b/src/controllers/api/oauth.ts index 4f1f495..a755a4d 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,10 +1,12 @@ +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'), @@ -60,25 +62,16 @@ 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); } const redirectUri = maybeDecodeUri(encodedUri); + const connectUri = await getClientConnectUri(c.req.raw.signal); - 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 + +
@@ -96,6 +103,8 @@ const oauthController: AppController = (c) => {
+
+ Nostr Connect `); 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/connect.ts b/src/utils/connect.ts new file mode 100644 index 0000000..8b3fdf8 --- /dev/null +++ b/src/utils/connect.ts @@ -0,0 +1,27 @@ +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, tagline } = await getInstanceMetadata(signal); + + const metadata: ConnectMetadata = { + name, + description: tagline, + url: Conf.localDomain, + }; + + uri.host = Conf.pubkey; + uri.searchParams.set('relay', Conf.relay); + uri.searchParams.set('metadata', JSON.stringify(metadata)); + + return uri.toString(); +} 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, + }; +}