Googstr/test/bunker.test.mjs
Jason Tudisco fcfaec675a Add NIP-46 signer, optional NIP-07 extension, passkey protection, backups
- NIP-46 remote signer in the page (bunker:// and nostrconnect:// pairing)
- Chrome extension (NIP-07) with per-site permission prompts, unlocked by
  the site via externally_connectable; downloadable from the page
- Passkey (WebAuthn PRF) encryption of the Drive key file
- NIP-49 backups: download or save to Drive; reveal nsec
- Fix: oldest key file wins when two devices sign up at once
- Fix: never overwrite an existing kind-0 because a relay was slow
- Fix: EOSE tracking; relays reconnect with backoff; verify event signatures
- One-click re-sign-in (consent only the first time)
- Vendored, pinned nostr-tools; CSP; no third-party scripts but Google's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:33:02 -06:00

91 lines
3.9 KiB
JavaScript

// 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");