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