Rework web signer to use NIP-46 events
This commit is contained in:
parent
1806cf2286
commit
655004e775
|
@ -0,0 +1,14 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { nip04 } from '@/deps.ts';
|
||||||
|
|
||||||
|
/** Encrypt a message as the Ditto server account. */
|
||||||
|
function encryptAdmin(targetPubkey: string, message: string): Promise<string> {
|
||||||
|
return nip04.encrypt(Conf.seckey, targetPubkey, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decrypt a message as the Ditto server account. */
|
||||||
|
function decryptAdmin(targetPubkey: string, message: string): Promise<string> {
|
||||||
|
return nip04.decrypt(Conf.seckey, targetPubkey, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { decryptAdmin, encryptAdmin };
|
|
@ -20,6 +20,7 @@ export {
|
||||||
getSignature,
|
getSignature,
|
||||||
Kind,
|
Kind,
|
||||||
matchFilters,
|
matchFilters,
|
||||||
|
nip04,
|
||||||
nip05,
|
nip05,
|
||||||
nip19,
|
nip19,
|
||||||
nip21,
|
nip21,
|
||||||
|
|
|
@ -80,12 +80,19 @@ const relayInfoDocSchema = z.object({
|
||||||
icon: safeUrlSchema.optional().catch(undefined),
|
icon: safeUrlSchema.optional().catch(undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** NIP-46 signer response. */
|
||||||
|
const connectResponseSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
result: signedEventSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ClientCLOSE,
|
type ClientCLOSE,
|
||||||
type ClientEVENT,
|
type ClientEVENT,
|
||||||
type ClientMsg,
|
type ClientMsg,
|
||||||
clientMsgSchema,
|
clientMsgSchema,
|
||||||
type ClientREQ,
|
type ClientREQ,
|
||||||
|
connectResponseSchema,
|
||||||
filterSchema,
|
filterSchema,
|
||||||
jsonMetaContentSchema,
|
jsonMetaContentSchema,
|
||||||
metaContentSchema,
|
metaContentSchema,
|
||||||
|
|
136
src/sign.ts
136
src/sign.ts
|
@ -1,68 +1,98 @@
|
||||||
import { type AppContext } from '@/app.ts';
|
import { type AppContext } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { type Event, type EventTemplate, finishEvent, HTTPException, z } from '@/deps.ts';
|
import { decryptAdmin, encryptAdmin } from '@/crypto.ts';
|
||||||
import { signedEventSchema } from '@/schemas/nostr.ts';
|
import { type Event, type EventTemplate, finishEvent, HTTPException } from '@/deps.ts';
|
||||||
import { ws } from '@/stream.ts';
|
import { connectResponseSchema } from '@/schemas/nostr.ts';
|
||||||
|
import { Sub } from '@/subs.ts';
|
||||||
/** Get signing WebSocket from app context. */
|
import { Time } from '@/utils.ts';
|
||||||
function getSignStream(c: AppContext): WebSocket | undefined {
|
import { createAdminEvent } from '@/utils/web.ts';
|
||||||
const pubkey = c.get('pubkey');
|
|
||||||
const session = c.get('session');
|
|
||||||
|
|
||||||
if (pubkey && session) {
|
|
||||||
const [socket] = ws.getSockets(`nostr:${pubkey}:${session}`);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nostrStreamingEventSchema = z.object({
|
|
||||||
type: z.literal('nostr.sign'),
|
|
||||||
data: signedEventSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign Nostr event using the app context.
|
* Sign Nostr event using the app context.
|
||||||
*
|
*
|
||||||
* - If a secret key is provided, it will be used to sign the event.
|
* - If a secret key is provided, it will be used to sign the event.
|
||||||
* - If a signing WebSocket is provided, it will be used to sign the event.
|
* - If `X-Nostr-Sign` is passed, it will use a NIP-46 to sign the event.
|
||||||
*/
|
*/
|
||||||
async function signEvent<K extends number = number>(event: EventTemplate<K>, c: AppContext): Promise<Event<K>> {
|
async function signEvent<K extends number = number>(event: EventTemplate<K>, c: AppContext): Promise<Event<K>> {
|
||||||
const seckey = c.get('seckey');
|
const seckey = c.get('seckey');
|
||||||
const stream = getSignStream(c);
|
const header = c.req.headers.get('x-nostr-sign');
|
||||||
|
|
||||||
if (!seckey && stream) {
|
|
||||||
try {
|
|
||||||
return await new Promise<Event<K>>((resolve, reject) => {
|
|
||||||
const handleMessage = (e: MessageEvent) => {
|
|
||||||
try {
|
|
||||||
const { data: event } = nostrStreamingEventSchema.parse(JSON.parse(e.data));
|
|
||||||
stream.removeEventListener('message', handleMessage);
|
|
||||||
resolve(event as Event<K>);
|
|
||||||
} catch (_e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
};
|
|
||||||
stream.addEventListener('message', handleMessage);
|
|
||||||
stream.send(JSON.stringify({ event: 'nostr.sign', payload: JSON.stringify(event) }));
|
|
||||||
setTimeout(() => {
|
|
||||||
stream.removeEventListener('message', handleMessage);
|
|
||||||
reject();
|
|
||||||
}, 60000);
|
|
||||||
});
|
|
||||||
} catch (_e) {
|
|
||||||
throw new HTTPException(408, {
|
|
||||||
res: c.json({ id: 'ditto.timeout', error: 'Signing timeout' }, 408),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seckey) {
|
|
||||||
throw new HTTPException(400, {
|
|
||||||
res: c.json({ id: 'ditto.private_key', error: 'No private key' }, 400),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (seckey) {
|
||||||
return finishEvent(event, seckey);
|
return finishEvent(event, seckey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
return await signNostrConnect(event, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<K extends number = number>(event: EventTemplate<K>, c: AppContext): Promise<Event<K>> {
|
||||||
|
const pubkey = c.get('pubkey');
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
throw new HTTPException(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = crypto.randomUUID();
|
||||||
|
|
||||||
|
createAdminEvent({
|
||||||
|
kind: 24133,
|
||||||
|
content: await encryptAdmin(
|
||||||
|
pubkey,
|
||||||
|
JSON.stringify({
|
||||||
|
id: messageId,
|
||||||
|
method: 'sign_event',
|
||||||
|
params: [event],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
tags: [['p', pubkey]],
|
||||||
|
}, c);
|
||||||
|
|
||||||
|
return awaitSignedEvent<K>(pubkey, messageId, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for signed event to be sent through Nostr relay. */
|
||||||
|
function awaitSignedEvent<K extends number = number>(
|
||||||
|
pubkey: string,
|
||||||
|
messageId: string,
|
||||||
|
c: AppContext,
|
||||||
|
): Promise<Event<K>> {
|
||||||
|
const sub = Sub.sub(messageId, '1', [{ kinds: [24133], authors: [pubkey], '#p': [Conf.pubkey] }]);
|
||||||
|
|
||||||
|
function close(): void {
|
||||||
|
Sub.close(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
close();
|
||||||
|
reject(
|
||||||
|
new HTTPException(408, {
|
||||||
|
res: c.json({ id: 'ditto.timeout', error: 'Signing timeout' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, Time.minutes(1));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for await (const event of sub) {
|
||||||
|
if (event.kind === 24133) {
|
||||||
|
const decrypted = await decryptAdmin(event.pubkey, event.content);
|
||||||
|
const msg = connectResponseSchema.parse(decrypted);
|
||||||
|
|
||||||
|
if (msg.id === messageId) {
|
||||||
|
close();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(msg.result as Event<K>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign event as the Ditto server. */
|
/** Sign event as the Ditto server. */
|
||||||
|
|
Loading…
Reference in New Issue