import { aesDecrypt, getTokenHash } from '@ditto/auth'; import { ConnectSigner, ReadOnlySigner } from '@ditto/signers'; import { HTTPException } from '@hono/hono/http-exception'; import { type NostrSigner, NSecSigner, type NStore } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import type { DittoMiddleware } from '../DittoMiddleware.ts'; interface User { signer: NostrSigner; store: NStore; } interface UserMiddlewareOpts { /** Returns a 401 response if no user can be determined. */ required?: boolean; /** Whether the user must prove themselves with a NIP-98 auth challenge. */ privileged: boolean; } // @ts-ignore The types are right. export function userMiddleware(opts: { privileged: boolean; required: true }): DittoMiddleware<{ user: User }>; export function userMiddleware(opts: { privileged: true; required?: boolean }): DittoMiddleware<{ user: User }>; export function userMiddleware(opts: { privileged: false; required?: boolean }): DittoMiddleware<{ user?: User }>; export function userMiddleware(opts: { privileged?: boolean; required?: boolean }): DittoMiddleware<{ user?: User }> { /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); return async (c, next) => { const { conf, db, store } = c.var; const header = c.req.header('authorization'); const match = header?.match(BEARER_REGEX); let signer: NostrSigner | undefined; if (match) { const [_, bech32] = match; if (bech32.startsWith('token1')) { try { const tokenHash = await getTokenHash(bech32 as `token1${string}`); const { pubkey: userPubkey, bunker_pubkey: bunkerPubkey, nip46_sk_enc, nip46_relays } = await db.kysely .selectFrom('auth_tokens') .select(['pubkey', 'bunker_pubkey', 'nip46_sk_enc', 'nip46_relays']) .where('token_hash', '=', tokenHash) .executeTakeFirstOrThrow(); const nep46Seckey = await aesDecrypt(conf.seckey, nip46_sk_enc); signer = new ConnectSigner({ bunkerPubkey, userPubkey, signer: new NSecSigner(nep46Seckey), relays: nip46_relays, relay: store, }); } catch { throw new HTTPException(401); } } else { try { const decoded = nip19.decode(bech32!); switch (decoded.type) { case 'npub': signer = new ReadOnlySigner(decoded.data); break; case 'nprofile': signer = new ReadOnlySigner(decoded.data.pubkey); break; case 'nsec': signer = new NSecSigner(decoded.data); break; } } catch { throw new HTTPException(401); } } } if (signer) { const user: User = { signer, store }; c.set('user', user); } else { throw new HTTPException(401); } await next(); }; }