tinyrelayfile
tinyrelay/scripts/qa/nip46-bunker.mjs/

scripts/qa/nip46-bunker.mjs raw
1#!/usr/bin/env node
2// A throwaway NIP-46 bunker for trying the Bunker URL sign-in flow locally.
3// It runs a minimal in-memory relay on 127.0.0.1:8789, answers connect,
4// ping, get_public_key and sign_event requests for a fresh user key, and
5// prints the bunker URL to paste into the sign-in page. Set BUNKER_USER_SECRET
6// to a hex secret to sign as a specific key.
7import { WebSocketServer, WebSocket } from "ws";
8import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools/pure";
9import * as nip44 from "nostr-tools/nip44";
10import { hexToBytes } from "@noble/hashes/utils.js";
11
12globalThis.WebSocket = WebSocket;
13
14const port = Number(process.env.BUNKER_PORT ?? 8789);
15const bunkerSecret = generateSecretKey();
16const bunkerPubkey = getPublicKey(bunkerSecret);
17const userSecret = process.env.BUNKER_USER_SECRET ? hexToBytes(process.env.BUNKER_USER_SECRET) : generateSecretKey();
18const userPubkey = getPublicKey(userSecret);
19const clients = new Map();
20const events = [];
21
22const matches = (event, filter) => {
23 if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
24 if (filter.authors && !filter.authors.includes(event.pubkey)) return false;
25 for (const [key, values] of Object.entries(filter)) {
26 if (key[0] === "#" && !event.tags.some(tag => tag[0] === key.slice(1) && values.includes(tag[1]))) return false;
27 }
28 return true;
29};
30
31const broadcast = event => {
32 events.push(event);
33 for (const [peer, subscriptions] of clients) {
34 for (const [id, filters] of subscriptions) {
35 if (filters.some(filter => matches(event, filter))) peer.send(JSON.stringify(["EVENT", id, event]));
36 }
37 }
38};
39
40const server = new WebSocketServer({ host: "127.0.0.1", port });
41server.on("connection", socket => {
42 const subscriptions = new Map();
43 clients.set(socket, subscriptions);
44 socket.on("message", raw => {
45 let message;
46 try { message = JSON.parse(raw); } catch { return; }
47 if (message[0] === "REQ") {
48 const [id, ...filters] = message.slice(1);
49 subscriptions.set(id, filters);
50 for (const event of events) {
51 if (filters.some(filter => matches(event, filter))) socket.send(JSON.stringify(["EVENT", id, event]));
52 }
53 socket.send(JSON.stringify(["EOSE", id]));
54 } else if (message[0] === "EVENT") {
55 broadcast(message[1]);
56 socket.send(JSON.stringify(["OK", message[1].id, true, ""]));
57 } else if (message[0] === "CLOSE") {
58 subscriptions.delete(message[1]);
59 }
60 });
61 socket.on("close", () => clients.delete(socket));
62});
63
64const reply = (clientPubkey, payload) => {
65 const conversation = nip44.getConversationKey(bunkerSecret, clientPubkey);
66 broadcast(finalizeEvent({
67 kind: 24133,
68 tags: [["p", clientPubkey]],
69 content: nip44.encrypt(JSON.stringify(payload), conversation),
70 created_at: Math.floor(Date.now() / 1000)
71 }, bunkerSecret));
72};
73
74const answer = request => {
75 switch (request.method) {
76 case "connect": return "ack";
77 case "ping": return "pong";
78 case "get_public_key": return userPubkey;
79 case "sign_event": return JSON.stringify(finalizeEvent(JSON.parse(request.params[0]), userSecret));
80 default: throw Error("unsupported method " + request.method);
81 }
82};
83
84const handled = new WeakSet();
85setInterval(() => {
86 for (const event of events) {
87 if (event.kind !== 24133 || event.pubkey === bunkerPubkey || handled.has(event)) continue;
88 handled.add(event);
89 try {
90 const conversation = nip44.getConversationKey(bunkerSecret, event.pubkey);
91 const request = JSON.parse(nip44.decrypt(event.content, conversation));
92 reply(event.pubkey, { id: request.id, result: answer(request) });
93 } catch (err) {
94 console.error("bunker request failed:", err.message);
95 }
96 }
97}, 50);
98
99console.log("user pubkey: " + userPubkey);
100console.log(`bunker://${bunkerPubkey}?relay=${encodeURIComponent("ws://127.0.0.1:" + port)}`);

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs