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
+
+
+
+ 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,
+ };
+}