clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

rewrite the NIP-46 bunker helper as a readable module

Dami committed 974ca1d
Use this checkout's dependencies, answer connect, ping, get_public_key
and sign_event, and print the bunker URL for the sign-in page.
commit
974ca1d9aef3def7b6a0e2a82d9c05a2d486a105
parent
a24bb5cd0b22c686bf0fde3d4fad9c16927b2e4b
author
Dami <dami.etoile@gmail.com>
1 file changed, +98 -9
Mscripts/qa/nip46-bunker.mjs107+++++++++++++++++++++++++++---
M scripts/qa/nip46-bunker.mjs +98 -9
@@ -1,9 +1,98 @@
1-const path=require('path');const {createRequire}=require('module');const deps=createRequire(path.resolve(__dirname,'../../../bindws/node_modules/'));const {WebSocketServer,WebSocket}=deps('ws'); global.WebSocket=WebSocket;2-const {generateSecretKey,getPublicKey,finalizeEvent}=deps('nostr-tools'); const nip44=require(path.join(path.dirname(deps.resolve('nostr-tools')),'nip44.js'));3-const wss=new WebSocketServer({port:8789}); const clients=new Map(),events=[]; const bunkerSK=generateSecretKey(), bunkerPK=getPublicKey(bunkerSK), userSK=generateSecretKey(), userPK=getPublicKey(userSK);4-function match(ev,f){if(f.kinds&&!f.kinds.includes(ev.kind))return false;if(f.authors&&!f.authors.includes(ev.pubkey))return false;for(const [k,vals] of Object.entries(f))if(k[0]==='#'&&!ev.tags.some(t=>t[0]===k.slice(1)&&vals.includes(t[1])))return false;return true}5-wss.on('connection',ws=>{const subs=new Map();clients.set(ws,subs);ws.on('message',raw=>{let m;try{m=JSON.parse(raw)}catch{return}if(m[0]==='REQ'){const [id,...fs]=m.slice(1);subs.set(id,fs);for(const ev of events)if(fs.some(f=>match(ev,f)))ws.send(JSON.stringify(['EVENT',id,ev]));ws.send(JSON.stringify(['EOSE',id]));}else if(m[0]==='EVENT'){const ev=m[1];events.push(ev);for(const [peer,ps] of clients)for(const [id,fs] of ps)if(fs.some(f=>match(ev,f)))peer.send(JSON.stringify(['EVENT',id,ev]));ws.send(JSON.stringify(['OK',ev.id,true,'']))}});ws.on('close',()=>clients.delete(ws));});6-function publish(obj,clientPK){const conv=nip44.getConversationKey(bunkerSK,clientPK);const ev=finalizeEvent({kind:24133,tags:[['p',clientPK]],content:nip44.encrypt(JSON.stringify(obj),conv),created_at:Math.floor(Date.now()/1000)},bunkerSK);events.push(ev);for(const [peer,ps] of clients)for(const [id,fs] of ps)if(fs.some(f=>match(ev,f)))peer.send(JSON.stringify(['EVENT',id,ev]));}7-setInterval(()=>{for(const ev of events){if(ev.kind!==24133||ev.pubkey===bunkerPK||ev._handled)continue;ev._handled=true;try{const clientPK=ev.pubkey,conv=nip44.getConversationKey(bunkerSK,clientPK),req=JSON.parse(nip44.decrypt(ev.content,conv));let result='';if(req.method==='connect')result='ack';else if(req.method==='ping')result='pong';else if(req.method==='get_public_key')result=userPK;else if(req.method==='sign_event'){const x=JSON.parse(req.params[0]);result=JSON.stringify(finalizeEvent(x,userSK));}publish({id:req.id,result},clientPK);}catch(e){console.error('handler',e.message)}}},50);8-console.log(`bunker://?relay=ws%3A%2F%2F127.0.0.1%3A8789&pubkey=${bunkerPK}`); // UI parser expects pubkey in host; print corrected below9-console.log(`bunker://${bunkerPK}?relay=ws%3A%2F%2F127.0.0.1%3A8789`);1+#!/usr/bin/env node2+// 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, and5+// prints the bunker URL to paste into the sign-in page.6+import { WebSocketServer, WebSocket } from "ws";7+import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools/pure";8+import * as nip44 from "nostr-tools/nip44";9+10+globalThis.WebSocket = WebSocket;11+12+const port = Number(process.env.BUNKER_PORT ?? 8789);13+const bunkerSecret = generateSecretKey();14+const bunkerPubkey = getPublicKey(bunkerSecret);15+const userSecret = generateSecretKey();16+const userPubkey = getPublicKey(userSecret);17+const clients = new Map();18+const events = [];19+20+const matches = (event, filter) => {21+ if (filter.kinds && !filter.kinds.includes(event.kind)) return false;22+ if (filter.authors && !filter.authors.includes(event.pubkey)) return false;23+ for (const [key, values] of Object.entries(filter)) {24+ if (key[0] === "#" && !event.tags.some(tag => tag[0] === key.slice(1) && values.includes(tag[1]))) return false;25+ }26+ return true;27+};28+29+const broadcast = event => {30+ events.push(event);31+ for (const [peer, subscriptions] of clients) {32+ for (const [id, filters] of subscriptions) {33+ if (filters.some(filter => matches(event, filter))) peer.send(JSON.stringify(["EVENT", id, event]));34+ }35+ }36+};37+38+const server = new WebSocketServer({ host: "127.0.0.1", port });39+server.on("connection", socket => {40+ const subscriptions = new Map();41+ clients.set(socket, subscriptions);42+ socket.on("message", raw => {43+ let message;44+ try { message = JSON.parse(raw); } catch { return; }45+ if (message[0] === "REQ") {46+ const [id, ...filters] = message.slice(1);47+ subscriptions.set(id, filters);48+ for (const event of events) {49+ if (filters.some(filter => matches(event, filter))) socket.send(JSON.stringify(["EVENT", id, event]));50+ }51+ socket.send(JSON.stringify(["EOSE", id]));52+ } else if (message[0] === "EVENT") {53+ broadcast(message[1]);54+ socket.send(JSON.stringify(["OK", message[1].id, true, ""]));55+ } else if (message[0] === "CLOSE") {56+ subscriptions.delete(message[1]);57+ }58+ });59+ socket.on("close", () => clients.delete(socket));60+});61+62+const reply = (clientPubkey, payload) => {63+ const conversation = nip44.getConversationKey(bunkerSecret, clientPubkey);64+ broadcast(finalizeEvent({65+ kind: 24133,66+ tags: [["p", clientPubkey]],67+ content: nip44.encrypt(JSON.stringify(payload), conversation),68+ created_at: Math.floor(Date.now() / 1000)69+ }, bunkerSecret));70+};71+72+const answer = request => {73+ switch (request.method) {74+ case "connect": return "ack";75+ case "ping": return "pong";76+ case "get_public_key": return userPubkey;77+ case "sign_event": return JSON.stringify(finalizeEvent(JSON.parse(request.params[0]), userSecret));78+ default: throw Error("unsupported method " + request.method);79+ }80+};81+82+const handled = new WeakSet();83+setInterval(() => {84+ for (const event of events) {85+ if (event.kind !== 24133 || event.pubkey === bunkerPubkey || handled.has(event)) continue;86+ handled.add(event);87+ try {88+ const conversation = nip44.getConversationKey(bunkerSecret, event.pubkey);89+ const request = JSON.parse(nip44.decrypt(event.content, conversation));90+ reply(event.pubkey, { id: request.id, result: answer(request) });91+ } catch (err) {92+ console.error("bunker request failed:", err.message);93+ }94+ }95+}, 50);96+97+console.log("user pubkey: " + userPubkey);98+console.log(`bunker://${bunkerPubkey}?relay=${encodeURIComponent("ws://127.0.0.1:" + port)}`);