// deno-lint-ignore-file require-await import { HTTPException } from '@hono/hono/http-exception'; import { NConnectSigner, type NostrEvent, type NostrSigner, type NRelay } from '@nostrify/nostrify'; interface ConnectSignerOpts { relay: NRelay; bunkerPubkey: string; userPubkey: string; signer: NostrSigner; relays?: string[]; } /** * NIP-46 signer. * * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ export class ConnectSigner implements NostrSigner { private signer: Promise; constructor(private opts: ConnectSignerOpts) { this.signer = this.init(opts.signer); } async init(signer: NostrSigner): Promise { return new NConnectSigner({ encryption: 'nip44', pubkey: this.opts.bunkerPubkey, relay: this.opts.relay, signer, timeout: 60_000, }); } async signEvent(event: Omit): Promise { const signer = await this.signer; try { return await signer.signEvent(event); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'The event was not signed quickly enough' }); } else { throw e; } } } readonly nip04 = { encrypt: async (pubkey: string, plaintext: string): Promise => { const signer = await this.signer; try { return await signer.nip04.encrypt(pubkey, plaintext); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'Text was not encrypted quickly enough', }); } else { throw e; } } }, decrypt: async (pubkey: string, ciphertext: string): Promise => { const signer = await this.signer; try { return await signer.nip04.decrypt(pubkey, ciphertext); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'Text was not decrypted quickly enough', }); } else { throw e; } } }, }; readonly nip44 = { encrypt: async (pubkey: string, plaintext: string): Promise => { const signer = await this.signer; try { return await signer.nip44.encrypt(pubkey, plaintext); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'Text was not encrypted quickly enough', }); } else { throw e; } } }, decrypt: async (pubkey: string, ciphertext: string): Promise => { const signer = await this.signer; try { return await signer.nip44.decrypt(pubkey, ciphertext); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'Text was not decrypted quickly enough', }); } else { throw e; } } }, }; // Prevent unnecessary NIP-46 round-trips. async getPublicKey(): Promise { return this.opts.userPubkey; } /** Get the user's relays if they passed in an `nprofile` auth token. */ async getRelays(): Promise> { return this.opts.relays?.reduce>((acc, relay) => { acc[relay] = { read: true, write: true }; return acc; }, {}) ?? {}; } }