import { NConnectSigner, NSchema as n, NSecSigner } from '@nostrify/nostrify'; import { bech32 } from '@scure/base'; import { escape } from 'entities'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { DittoDB } from '@/db/DittoDB.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; import { Storages } from '@/storages.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), password: z.string(), }); const codeGrantSchema = z.object({ grant_type: z.literal('authorization_code'), code: z.string(), }); const credentialsGrantSchema = z.object({ grant_type: z.literal('client_credentials'), }); const nostrGrantSchema = z.object({ grant_type: z.literal('nostr_bunker'), pubkey: n.id(), relays: z.string().url().array().optional(), secret: z.string().optional(), }); const createTokenSchema = z.discriminatedUnion('grant_type', [ passwordGrantSchema, codeGrantSchema, credentialsGrantSchema, nostrGrantSchema, ]); const createTokenController: AppController = async (c) => { const body = await parseBody(c.req.raw); const result = createTokenSchema.safeParse(body); if (!result.success) { return c.json({ error: 'Invalid request', issues: result.error.issues }, 400); } switch (result.data.grant_type) { case 'nostr_bunker': return c.json({ access_token: await getToken(result.data), token_type: 'Bearer', scope: 'read write follow push', created_at: nostrNow(), }); case 'password': return c.json({ access_token: result.data.password, token_type: 'Bearer', scope: 'read write follow push', created_at: nostrNow(), }); case 'authorization_code': return c.json({ access_token: result.data.code, token_type: 'Bearer', scope: 'read write follow push', created_at: nostrNow(), }); case 'client_credentials': return c.json({ access_token: '_', token_type: 'Bearer', scope: 'read write follow push', created_at: nostrNow(), }); } }; async function getToken( { pubkey, secret, relays = [] }: { pubkey: string; secret?: string; relays?: string[] }, ): Promise<`token1${string}`> { const kysely = await DittoDB.getInstance(); const token = generateToken(); const serverSeckey = generateSecretKey(); const serverPubkey = getPublicKey(serverSeckey); const signer = new NConnectSigner({ pubkey, signer: new NSecSigner(serverSeckey), relay: await Storages.pubsub(), // TODO: Use the relays from the request. timeout: 60_000, }); await signer.connect(secret); await kysely.insertInto('nip46_tokens').values({ api_token: token, user_pubkey: pubkey, server_seckey: serverSeckey, server_pubkey: serverPubkey, relays: JSON.stringify(relays), connected_at: new Date(), }).execute(); return token; } /** Generate a bech32 token for the API. */ function generateToken(): `token1${string}` { const words = bech32.toWords(generateSecretKey()); return bech32.encode('token', words); } /** Display the OAuth form. */ const oauthController: AppController = (c) => { const encodedUri = c.req.query('redirect_uri'); if (!encodedUri) { return c.text('Missing `redirect_uri` query param.', 422); } const redirectUri = maybeDecodeUri(encodedUri); return c.html(`