Add APISigner and AdminSigner classes, implement NostrSigner interface
This commit is contained in:
parent
1e3f637358
commit
59d53c4a2f
|
@ -1,5 +1,5 @@
|
|||
import * as pipeline from '@/pipeline.ts';
|
||||
import { signAdminEvent } from '@/sign.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { type EventStub } from '@/utils/api.ts';
|
||||
import { nostrNow } from '@/utils.ts';
|
||||
|
||||
|
@ -12,7 +12,9 @@ switch (Deno.args[0]) {
|
|||
}
|
||||
|
||||
async function publish(t: EventStub) {
|
||||
const event = await signAdminEvent({
|
||||
const signer = new AdminSigner();
|
||||
|
||||
const event = await signer.signEvent({
|
||||
content: '',
|
||||
created_at: nostrNow(),
|
||||
tags: [],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Conf } from '@/config.ts';
|
||||
import { Debug, type NostrFilter } from '@/deps.ts';
|
||||
import * as pipeline from '@/pipeline.ts';
|
||||
import { signAdminEvent } from '@/sign.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
|
||||
const debug = Debug('ditto:users');
|
||||
|
@ -15,8 +15,9 @@ interface User {
|
|||
|
||||
function buildUserEvent(user: User) {
|
||||
const { origin, host } = Conf.url;
|
||||
const signer = new AdminSigner();
|
||||
|
||||
return signAdminEvent({
|
||||
return signer.signEvent({
|
||||
kind: 30361,
|
||||
tags: [
|
||||
['d', user.pubkey],
|
||||
|
|
|
@ -81,6 +81,7 @@ export * as Comlink from 'npm:comlink@^4.4.1';
|
|||
export { EventEmitter } from 'npm:tseep@^1.1.3';
|
||||
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
||||
export { default as Debug } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.2.0/debug.ts';
|
||||
export { Stickynotes } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.2.0/mod.ts';
|
||||
export {
|
||||
LNURL,
|
||||
type LNURLDetails,
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
validateAuthEvent,
|
||||
} from '@/utils/nip98.ts';
|
||||
import { localRequest } from '@/utils/api.ts';
|
||||
import { signEvent } from '@/sign.ts';
|
||||
import { APISigner } from '@/signers/APISigner.ts';
|
||||
import { findUser, User } from '@/db/users.ts';
|
||||
|
||||
/**
|
||||
|
@ -91,7 +91,7 @@ function withProof(
|
|||
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
||||
const req = localRequest(c);
|
||||
const reqEvent = await buildAuthEventTemplate(req, opts);
|
||||
const resEvent = await signEvent(reqEvent, c, opts);
|
||||
const resEvent = await new APISigner(c).signEvent(reqEvent);
|
||||
const result = await validateAuthEvent(req, resEvent, opts);
|
||||
|
||||
if (result.success) {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { eventAge, isRelay, nostrDate, nostrNow, Time } from '@/utils.ts';
|
|||
import { fetchWorker } from '@/workers/fetch.ts';
|
||||
import { TrendsWorker } from '@/workers/trends.ts';
|
||||
import { verifyEventWorker } from '@/workers/verify.ts';
|
||||
import { signAdminEvent } from '@/sign.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
||||
|
||||
const debug = Debug('ditto:pipeline');
|
||||
|
@ -194,7 +194,9 @@ async function payZap(event: DittoEvent, signal: AbortSignal) {
|
|||
{ fetch: fetchWorker, signal },
|
||||
);
|
||||
|
||||
const nwcRequestEvent = await signAdminEvent({
|
||||
const signer = new AdminSigner();
|
||||
|
||||
const nwcRequestEvent = await signer.signEvent({
|
||||
kind: 23194,
|
||||
content: await encryptAdmin(
|
||||
event.pubkey,
|
||||
|
|
121
src/sign.ts
121
src/sign.ts
|
@ -1,121 +0,0 @@
|
|||
import { type AppContext } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { decryptAdmin, encryptAdmin } from '@/crypto.ts';
|
||||
import { Debug, type EventTemplate, finalizeEvent, HTTPException, type NostrEvent } from '@/deps.ts';
|
||||
import { connectResponseSchema } from '@/schemas/nostr.ts';
|
||||
import { jsonSchema } from '@/schema.ts';
|
||||
import { Sub } from '@/subs.ts';
|
||||
import { eventMatchesTemplate } from '@/utils.ts';
|
||||
import { createAdminEvent } from '@/utils/api.ts';
|
||||
|
||||
const debug = Debug('ditto:sign');
|
||||
|
||||
interface SignEventOpts {
|
||||
/** Target proof-of-work difficulty for the signed event. */
|
||||
pow?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign Nostr event using the app context.
|
||||
*
|
||||
* - If a secret key is provided, it will be used to sign the event.
|
||||
* - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
|
||||
*/
|
||||
async function signEvent(
|
||||
event: EventTemplate,
|
||||
c: AppContext,
|
||||
opts: SignEventOpts = {},
|
||||
): Promise<NostrEvent> {
|
||||
const seckey = c.get('seckey');
|
||||
const header = c.req.header('x-nostr-sign');
|
||||
|
||||
if (seckey) {
|
||||
debug(`Signing Event<${event.kind}> with secret key`);
|
||||
return finalizeEvent(event, seckey);
|
||||
}
|
||||
|
||||
if (header) {
|
||||
debug(`Signing Event<${event.kind}> with NIP-46`);
|
||||
return await signNostrConnect(event, c, opts);
|
||||
}
|
||||
|
||||
throw new HTTPException(400, {
|
||||
res: c.json({ id: 'ditto.sign', error: 'Unable to sign event' }, 400),
|
||||
});
|
||||
}
|
||||
|
||||
/** Sign event with NIP-46, waiting in the background for the signed event. */
|
||||
async function signNostrConnect(
|
||||
event: EventTemplate,
|
||||
c: AppContext,
|
||||
opts: SignEventOpts = {},
|
||||
): Promise<NostrEvent> {
|
||||
const pubkey = c.get('pubkey');
|
||||
|
||||
if (!pubkey) {
|
||||
throw new HTTPException(401, { message: 'Missing pubkey' });
|
||||
}
|
||||
|
||||
const messageId = crypto.randomUUID();
|
||||
|
||||
createAdminEvent({
|
||||
kind: 24133,
|
||||
content: await encryptAdmin(
|
||||
pubkey,
|
||||
JSON.stringify({
|
||||
id: messageId,
|
||||
method: 'sign_event',
|
||||
params: [event, {
|
||||
pow: opts.pow,
|
||||
}],
|
||||
}),
|
||||
),
|
||||
tags: [['p', pubkey]],
|
||||
}, c);
|
||||
|
||||
return awaitSignedEvent(pubkey, messageId, event, c);
|
||||
}
|
||||
|
||||
/** Wait for signed event to be sent through Nostr relay. */
|
||||
async function awaitSignedEvent(
|
||||
pubkey: string,
|
||||
messageId: string,
|
||||
template: EventTemplate,
|
||||
c: AppContext,
|
||||
): Promise<NostrEvent> {
|
||||
const sub = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]);
|
||||
|
||||
function close(): void {
|
||||
Sub.close(messageId);
|
||||
c.req.raw.signal.removeEventListener('abort', close);
|
||||
}
|
||||
|
||||
c.req.raw.signal.addEventListener('abort', close);
|
||||
|
||||
for await (const event of sub) {
|
||||
const decrypted = await decryptAdmin(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: c.json({ id: 'ditto.timeout', error: 'Signing timeout' }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Sign event as the Ditto server. */
|
||||
// deno-lint-ignore require-await
|
||||
async function signAdminEvent(event: EventTemplate): Promise<NostrEvent> {
|
||||
return finalizeEvent(event, Conf.seckey);
|
||||
}
|
||||
|
||||
export { signAdminEvent, signEvent };
|
|
@ -0,0 +1,114 @@
|
|||
import { type AppContext } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { decryptAdmin, encryptAdmin } from '@/crypto.ts';
|
||||
import { HTTPException, type NostrEvent, type NostrSigner, NSecSigner, Stickynotes } from '@/deps.ts';
|
||||
import { connectResponseSchema } from '@/schemas/nostr.ts';
|
||||
import { jsonSchema } from '@/schema.ts';
|
||||
import { Sub } from '@/subs.ts';
|
||||
import { eventMatchesTemplate } from '@/utils.ts';
|
||||
import { createAdminEvent } from '@/utils/api.ts';
|
||||
|
||||
/**
|
||||
* Sign Nostr event using the app context.
|
||||
*
|
||||
* - If a secret key is provided, it will be used to sign the event.
|
||||
* - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
|
||||
*/
|
||||
export class APISigner implements NostrSigner {
|
||||
#c: AppContext;
|
||||
#console = new Stickynotes('ditto:sign');
|
||||
|
||||
constructor(c: AppContext) {
|
||||
this.#c = c;
|
||||
}
|
||||
|
||||
// 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');
|
||||
const header = this.#c.req.header('x-nostr-sign');
|
||||
|
||||
if (seckey) {
|
||||
this.#console.debug(`Signing Event<${event.kind}> with secret key`);
|
||||
return new NSecSigner(seckey).signEvent(event);
|
||||
}
|
||||
|
||||
if (header) {
|
||||
this.#console.debug(`Signing Event<${event.kind}> with NIP-46`);
|
||||
return await this.#signNostrConnect(event);
|
||||
}
|
||||
|
||||
throw new HTTPException(400, {
|
||||
res: this.#c.json({ id: 'ditto.sign', error: 'Unable to sign event' }, 400),
|
||||
});
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
throw new HTTPException(401, { message: 'Missing pubkey' });
|
||||
}
|
||||
|
||||
const messageId = crypto.randomUUID();
|
||||
|
||||
createAdminEvent({
|
||||
kind: 24133,
|
||||
content: await encryptAdmin(
|
||||
pubkey,
|
||||
JSON.stringify({
|
||||
id: messageId,
|
||||
method: 'sign_event',
|
||||
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 decryptAdmin(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' }),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Conf } from '@/config.ts';
|
||||
import { NSecSigner } from '@/deps.ts';
|
||||
|
||||
/** Sign events as the Ditto server. */
|
||||
export class AdminSigner extends NSecSigner {
|
||||
constructor() {
|
||||
super(Conf.seckey);
|
||||
}
|
||||
}
|
|
@ -12,7 +12,8 @@ import {
|
|||
z,
|
||||
} from '@/deps.ts';
|
||||
import * as pipeline from '@/pipeline.ts';
|
||||
import { signAdminEvent, signEvent } from '@/sign.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { APISigner } from '@/signers/APISigner.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { nostrNow } from '@/utils.ts';
|
||||
|
||||
|
@ -29,12 +30,14 @@ async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
|||
throw new HTTPException(401);
|
||||
}
|
||||
|
||||
const event = await signEvent({
|
||||
const signer = new APISigner(c);
|
||||
|
||||
const event = await signer.signEvent({
|
||||
content: '',
|
||||
created_at: nostrNow(),
|
||||
tags: [],
|
||||
...t,
|
||||
}, c);
|
||||
});
|
||||
|
||||
return publishEvent(event, c);
|
||||
}
|
||||
|
@ -70,7 +73,9 @@ function updateListEvent(
|
|||
|
||||
/** Publish an admin event through the pipeline. */
|
||||
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||
const event = await signAdminEvent({
|
||||
const signer = new AdminSigner();
|
||||
|
||||
const event = await signer.signEvent({
|
||||
content: '',
|
||||
created_at: nostrNow(),
|
||||
tags: [],
|
||||
|
|
Loading…
Reference in New Issue