Merge branch 'refactor-subs' into 'main'

Add full NIP-46 signer from Nostrify

See merge request soapbox-pub/ditto!185
This commit is contained in:
Alex Gleason 2024-04-26 03:04:17 +00:00
commit f3bc12252a
2 changed files with 43 additions and 85 deletions

View File

@ -16,7 +16,7 @@
"exclude": ["./public"], "exclude": ["./public"],
"imports": { "imports": {
"@/": "./src/", "@/": "./src/",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.14.2", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.14.3",
"@std/cli": "jsr:@std/cli@^0.223.0", "@std/cli": "jsr:@std/cli@^0.223.0",
"@std/json": "jsr:@std/json@^0.223.0", "@std/json": "jsr:@std/json@^0.223.0",
"@std/streams": "jsr:@std/streams@^0.223.0", "@std/streams": "jsr:@std/streams@^0.223.0",

View File

@ -1,14 +1,10 @@
import { NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify'; // deno-lint-ignore-file require-await
import { NConnectSigner, NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify';
import { HTTPException } from 'hono'; import { HTTPException } from 'hono';
import { type AppContext } from '@/app.ts'; import { type AppContext } from '@/app.ts';
import { Conf } from '@/config.ts';
import { Stickynotes } from '@/deps.ts';
import { connectResponseSchema } from '@/schemas/nostr.ts';
import { jsonSchema } from '@/schema.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { eventMatchesTemplate } from '@/utils.ts';
import { createAdminEvent } from '@/utils/api.ts';
/** /**
* Sign Nostr event using the app context. * Sign Nostr event using the app context.
@ -17,91 +13,53 @@ import { createAdminEvent } from '@/utils/api.ts';
* - Otherwise, it will use NIP-46 to sign the event. * - Otherwise, it will use NIP-46 to sign the event.
*/ */
export class APISigner implements NostrSigner { export class APISigner implements NostrSigner {
#c: AppContext; private signer: NostrSigner;
#console = new Stickynotes('ditto:sign');
constructor(c: AppContext) { constructor(c: AppContext) {
this.#c = c; const seckey = c.get('seckey');
} const pubkey = c.get('pubkey');
// deno-lint-ignore require-await
async getPublicKey(): Promise<string> {
const pubkey = this.#c.get('pubkey');
if (pubkey) {
return pubkey;
} else {
throw new HTTPException(401, { message: 'Missing pubkey' });
}
}
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
const seckey = this.#c.get('seckey');
if (seckey) {
this.#console.debug(`Signing Event<${event.kind}> with secret key`);
return new NSecSigner(seckey).signEvent(event);
}
this.#console.debug(`Signing Event<${event.kind}> with NIP-46`);
return await this.#signNostrConnect(event);
}
/** Sign event with NIP-46, waiting in the background for the signed event. */
async #signNostrConnect(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
const pubkey = this.#c.get('pubkey');
if (!pubkey) { if (!pubkey) {
throw new HTTPException(401, { message: 'Missing pubkey' }); throw new HTTPException(401, { message: 'Missing pubkey' });
} }
const messageId = crypto.randomUUID(); if (seckey) {
this.signer = new NSecSigner(seckey);
createAdminEvent({ } else {
kind: 24133, this.signer = new NConnectSigner({
content: await new AdminSigner().nip04.encrypt(
pubkey, pubkey,
JSON.stringify({ relay: Storages.pubsub,
id: messageId, signer: new AdminSigner(),
method: 'sign_event', timeout: 60000,
params: [event],
}),
),
tags: [['p', pubkey]],
}, this.#c);
return this.#awaitSignedEvent(pubkey, messageId, event);
}
/** Wait for signed event to be sent through Nostr relay. */
async #awaitSignedEvent(
pubkey: string,
messageId: string,
template: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>,
): Promise<NostrEvent> {
const sub = Storages.pubsub.req(
[{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }],
{ signal: this.#c.req.raw.signal },
);
for await (const msg of sub) {
if (msg[0] === 'EVENT') {
const event = msg[2];
const decrypted = await new AdminSigner().nip04.decrypt(event.pubkey, event.content);
const result = jsonSchema
.pipe(connectResponseSchema)
.refine((msg) => msg.id === messageId, 'Message ID mismatch')
.refine((msg) => eventMatchesTemplate(msg.result, template), 'Event template mismatch')
.safeParse(decrypted);
if (result.success) {
return result.data.result;
}
}
}
throw new HTTPException(408, {
res: this.#c.json({ id: 'ditto.timeout', error: 'Signing timeout' }),
}); });
} }
} }
async getPublicKey(): Promise<string> {
return this.signer.getPublicKey();
}
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
return this.signer.signEvent(event);
}
readonly nip04 = {
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
return this.signer.nip04!.encrypt(pubkey, plaintext);
},
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
return this.signer.nip04!.decrypt(pubkey, ciphertext);
},
};
readonly nip44 = {
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
return this.signer.nip44!.encrypt(pubkey, plaintext);
},
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
return this.signer.nip44!.decrypt(pubkey, ciphertext);
},
};
}