// Self-check for the NIP-46 signer: node test/bunker.test.mjs // Plays a client app against createBunker() through an in-memory "relay". import assert from "node:assert/strict"; import { generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, nip44 } from "../public/vendor/nostr-tools.js"; const memStore = () => { const m = new Map(); return { getItem: (k) => m.get(k) ?? null, setItem: (k, v) => m.set(k, String(v)), removeItem: (k) => m.delete(k) }; }; globalThis.localStorage = memStore(); globalThis.sessionStorage = memStore(); globalThis.addEventListener = () => {}; globalThis.document = { addEventListener() {} }; const { makeSigner } = await import("../public/goostr.js"); const { createBunker } = await import("../public/bunker.js"); // In-memory pool: subscribe() captures the handler, publish() collects replies. const published = []; let deliver = null; const pool = { subscribe: (_f, onEvent) => { deliver = onEvent; return () => { deliver = null; }; }, publish: (ev) => published.push(ev), addRelay() {}, }; const userSk = generateSecretKey(); const signer = makeSigner(userSk); const bunker = createBunker({ signer, pool, relays: ["wss://relay.example"] }); bunker.start(); function client() { const sk = generateSecretKey(); const pk = getPublicKey(sk); const ck = nip44.getConversationKey(sk, signer.pubkey); return { pk, async call(method, params) { const id = Math.random().toString(36).slice(2); const req = finalizeEvent({ kind: 24133, created_at: Math.floor(Date.now() / 1000), tags: [["p", signer.pubkey]], content: nip44.encrypt(JSON.stringify({ id, method, params }), ck) }, sk); const before = published.length; await deliver(req); await new Promise((r) => setTimeout(r, 20)); const res = published.slice(before).find((e) => e.tags.some((t) => t[0] === "p" && t[1] === pk)); assert.ok(res, `no reply to ${method}`); assert.ok(verifyEvent(res)); assert.equal(res.pubkey, signer.pubkey); const body = JSON.parse(nip44.decrypt(res.content, ck)); assert.equal(body.id, id); return body; }, }; } // bunker:// pairing const url = new URL(bunker.newBunkerUrl().replace("bunker://", "https://")); assert.equal(url.hostname, signer.pubkey); const secret = url.searchParams.get("secret"); const app = client(); assert.match((await app.call("get_public_key", [])).error, /unauthorized/, "unpaired app must be refused"); assert.match((await app.call("connect", [signer.pubkey, "wrong"])).error, /unauthorized/); assert.equal((await app.call("connect", [signer.pubkey, secret])).result, "ack"); assert.equal((await app.call("get_public_key", [])).result, signer.pubkey); assert.equal((await app.call("ping", [])).result, "pong"); const signed = JSON.parse((await app.call("sign_event", [JSON.stringify({ kind: 1, content: "hi", tags: [], created_at: 1 })])).result); assert.ok(verifyEvent(signed)); assert.equal(signed.pubkey, signer.pubkey); const other = client(); const ct = (await app.call("nip44_encrypt", [other.pk, "secret msg"])).result; assert.equal((await app.call("nip44_decrypt", [other.pk, ct])).result, "secret msg"); // the secret is single-use const thief = client(); assert.match((await thief.call("connect", [signer.pubkey, secret])).error, /unauthorized/); // revoke bunker.revoke(app.pk); assert.match((await app.call("get_public_key", [])).error, /unauthorized/); // nostrconnect:// pairing: we must answer with the app's secret const ncSk = generateSecretKey(); const ncPk = getPublicKey(ncSk); const before = published.length; await bunker.acceptNostrConnect(`nostrconnect://${ncPk}?relay=wss%3A%2F%2Frelay.example&secret=s3cr3t&name=Test%20App`); const ack = published.slice(before)[0]; assert.equal(JSON.parse(nip44.decrypt(ack.content, nip44.getConversationKey(ncSk, signer.pubkey))).result, "s3cr3t"); assert.equal(bunker.clients[ncPk].name, "Test App"); console.log("bunker: all checks passed");