APISigner: reimplement with NConnectSigner and InternalRelay
This commit is contained in:
parent
8407583d83
commit
fc47116dd3
|
@ -16,7 +16,7 @@
|
||||||
"exclude": ["./public"],
|
"exclude": ["./public"],
|
||||||
"imports": {
|
"imports": {
|
||||||
"@/": "./src/",
|
"@/": "./src/",
|
||||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.12.1",
|
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.14.0",
|
||||||
"@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",
|
||||||
|
|
|
@ -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 { Sub } from '@/subs.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,93 +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 = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]);
|
|
||||||
|
|
||||||
const close = (): void => {
|
|
||||||
Sub.close(messageId);
|
|
||||||
this.#c.req.raw.signal.removeEventListener('abort', close);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#c.req.raw.signal.addEventListener('abort', close);
|
|
||||||
|
|
||||||
for await (const event of sub) {
|
|
||||||
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) {
|
|
||||||
close();
|
|
||||||
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Optimizer } from '@/storages/optimizer.ts';
|
||||||
import { PoolStore } from '@/storages/pool-store.ts';
|
import { PoolStore } from '@/storages/pool-store.ts';
|
||||||
import { Reqmeister } from '@/storages/reqmeister.ts';
|
import { Reqmeister } from '@/storages/reqmeister.ts';
|
||||||
import { SearchStore } from '@/storages/search-store.ts';
|
import { SearchStore } from '@/storages/search-store.ts';
|
||||||
|
import { InternalRelay } from '@/storages/InternalRelay.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
||||||
/** Relay pool storage. */
|
/** Relay pool storage. */
|
||||||
|
@ -43,4 +44,16 @@ const searchStore = new SearchStore({
|
||||||
fallback: optimizer,
|
fallback: optimizer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export class Storages {
|
||||||
|
private static _subsub: InternalRelay | undefined;
|
||||||
|
|
||||||
|
static get pubsub(): InternalRelay {
|
||||||
|
if (!this._subsub) {
|
||||||
|
this._subsub = new InternalRelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._subsub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { cache, client, eventsDB, optimizer, reqmeister, searchStore };
|
export { cache, client, eventsDB, optimizer, reqmeister, searchStore };
|
||||||
|
|
Loading…
Reference in New Issue