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 * as pipeline from '@/pipeline.ts';
|
||||||
import { signAdminEvent } from '@/sign.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { type EventStub } from '@/utils/api.ts';
|
import { type EventStub } from '@/utils/api.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
|
|
||||||
|
@ -12,7 +12,9 @@ switch (Deno.args[0]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publish(t: EventStub) {
|
async function publish(t: EventStub) {
|
||||||
const event = await signAdminEvent({
|
const signer = new AdminSigner();
|
||||||
|
|
||||||
|
const event = await signer.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { Debug, type NostrFilter } from '@/deps.ts';
|
import { Debug, type NostrFilter } from '@/deps.ts';
|
||||||
import * as pipeline from '@/pipeline.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { signAdminEvent } from '@/sign.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { eventsDB } from '@/storages.ts';
|
import { eventsDB } from '@/storages.ts';
|
||||||
|
|
||||||
const debug = Debug('ditto:users');
|
const debug = Debug('ditto:users');
|
||||||
|
@ -15,8 +15,9 @@ interface User {
|
||||||
|
|
||||||
function buildUserEvent(user: User) {
|
function buildUserEvent(user: User) {
|
||||||
const { origin, host } = Conf.url;
|
const { origin, host } = Conf.url;
|
||||||
|
const signer = new AdminSigner();
|
||||||
|
|
||||||
return signAdminEvent({
|
return signer.signEvent({
|
||||||
kind: 30361,
|
kind: 30361,
|
||||||
tags: [
|
tags: [
|
||||||
['d', user.pubkey],
|
['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 { EventEmitter } from 'npm:tseep@^1.1.3';
|
||||||
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
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 { 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 {
|
export {
|
||||||
LNURL,
|
LNURL,
|
||||||
type LNURLDetails,
|
type LNURLDetails,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
validateAuthEvent,
|
validateAuthEvent,
|
||||||
} from '@/utils/nip98.ts';
|
} from '@/utils/nip98.ts';
|
||||||
import { localRequest } from '@/utils/api.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';
|
import { findUser, User } from '@/db/users.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +91,7 @@ function withProof(
|
||||||
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
||||||
const req = localRequest(c);
|
const req = localRequest(c);
|
||||||
const reqEvent = await buildAuthEventTemplate(req, opts);
|
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);
|
const result = await validateAuthEvent(req, resEvent, opts);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { eventAge, isRelay, nostrDate, nostrNow, Time } from '@/utils.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
import { TrendsWorker } from '@/workers/trends.ts';
|
import { TrendsWorker } from '@/workers/trends.ts';
|
||||||
import { verifyEventWorker } from '@/workers/verify.ts';
|
import { verifyEventWorker } from '@/workers/verify.ts';
|
||||||
import { signAdminEvent } from '@/sign.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
import { lnurlCache } from '@/utils/lnurl.ts';
|
||||||
|
|
||||||
const debug = Debug('ditto:pipeline');
|
const debug = Debug('ditto:pipeline');
|
||||||
|
@ -194,7 +194,9 @@ async function payZap(event: DittoEvent, signal: AbortSignal) {
|
||||||
{ fetch: fetchWorker, signal },
|
{ fetch: fetchWorker, signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
const nwcRequestEvent = await signAdminEvent({
|
const signer = new AdminSigner();
|
||||||
|
|
||||||
|
const nwcRequestEvent = await signer.signEvent({
|
||||||
kind: 23194,
|
kind: 23194,
|
||||||
content: await encryptAdmin(
|
content: await encryptAdmin(
|
||||||
event.pubkey,
|
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,
|
z,
|
||||||
} from '@/deps.ts';
|
} from '@/deps.ts';
|
||||||
import * as pipeline from '@/pipeline.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 { eventsDB } from '@/storages.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
|
|
||||||
|
@ -29,12 +30,14 @@ async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
throw new HTTPException(401);
|
throw new HTTPException(401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await signEvent({
|
const signer = new APISigner(c);
|
||||||
|
|
||||||
|
const event = await signer.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
tags: [],
|
tags: [],
|
||||||
...t,
|
...t,
|
||||||
}, c);
|
});
|
||||||
|
|
||||||
return publishEvent(event, c);
|
return publishEvent(event, c);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +73,9 @@ function updateListEvent(
|
||||||
|
|
||||||
/** Publish an admin event through the pipeline. */
|
/** Publish an admin event through the pipeline. */
|
||||||
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
const event = await signAdminEvent({
|
const signer = new AdminSigner();
|
||||||
|
|
||||||
|
const event = await signer.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
Loading…
Reference in New Issue