mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-01 19:11:23 +00:00
Implements login-methods.md §LM5 phase one. Core gains nostr/nip46.ts -- a Nip46Client speaking kind-24133 RPC with NIP-44 transport encryption, bunker:// parsing, nostrconnect listen (secret echo is proof-of-scan), auth_url surfacing, and typed deadline errors -- plus subscribeLive on RelayConnection with re-REQ on reconnect, and live post-EOSE delivery in the MockRelay so tests exercise real round trips against a fake bunker. The ephemeral client key and Nip46Client live in the crypto worker (§LM5.4); nip46* actions bypass the serial queue so a signer waiting minutes for human approval cannot block other worker calls, and the signer's auth_url reaches the UI as the worker's first notification frame. The widget's connect screen runs both legs at once: a nostrconnect QR -- rendered as dot-style inline SVG with rounded finder eyes, per owner preference -- for phone signers, and a bunker:// paste box (password input; the URI carries a token). window.nostr is now an element-routed provider, so a NIP-46 session claims the slot and hosts reading it get the remote signer transparently; NIP-07 still leaves the slot to the extension. Sessions stay thin per §LM7: no vault, no persistence. Persistence remains gated on the external vault review (§LM5.3). 14 new tests (core fake-bunker e2e, QR geometry, signer delegation) and two policy pins: bunker option below the password path, element stays web-storage-free. Co-Authored-By: Claude Fable 5 <[email protected]>
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import qrcodeGenerator from "qrcode-generator";
|
|
import { renderQrSvg } from "./qr.js";
|
|
|
|
const SAMPLE =
|
|
"nostrconnect://" +
|
|
"ab".repeat(32) +
|
|
"?relay=wss%3A%2F%2Frelay.example&secret=0123456789abcdef&name=BitLogin";
|
|
|
|
describe("renderQrSvg", () => {
|
|
it("renders a complete standalone SVG with backing, eyes, and dots", () => {
|
|
const svg = renderQrSvg(SAMPLE);
|
|
expect(svg.startsWith("<svg ")).toBe(true);
|
|
expect(svg.endsWith("</svg>")).toBe(true);
|
|
// One opaque backing rect plus three finder eyes of two rects each.
|
|
expect(svg.match(/<rect /gu)!.length).toBe(1 + 3 * 2);
|
|
// Data modules are circles -- the "dots" -- and there are plenty of them.
|
|
expect(svg.match(/<circle /gu)!.length).toBeGreaterThan(50);
|
|
});
|
|
|
|
it("draws exactly the dark data modules the encoder produced, minus finder zones", () => {
|
|
const qr = qrcodeGenerator(0, "M");
|
|
qr.addData(SAMPLE);
|
|
qr.make();
|
|
const n = qr.getModuleCount();
|
|
let expected = 0;
|
|
const inEye = (r: number, c: number) =>
|
|
(r < 7 && c < 7) || (r < 7 && c >= n - 7) || (r >= n - 7 && c < 7);
|
|
for (let r = 0; r < n; r++) {
|
|
for (let c = 0; c < n; c++) {
|
|
if (!inEye(r, c) && qr.isDark(r, c)) expected++;
|
|
}
|
|
}
|
|
const svg = renderQrSvg(SAMPLE);
|
|
expect(svg.match(/<circle /gu)!.length).toBe(expected);
|
|
});
|
|
|
|
it("keeps the quiet zone: no dot within four modules of the edge", () => {
|
|
const svg = renderQrSvg(SAMPLE);
|
|
const centers = [...svg.matchAll(/<circle cx="([\d.]+)" cy="([\d.]+)"/gu)].map((m) => [
|
|
Number(m[1]),
|
|
Number(m[2])
|
|
]);
|
|
const viewBox = /viewBox="0 0 (\d+) (\d+)"/u.exec(svg)!;
|
|
const size = Number(viewBox[1]);
|
|
for (const [cx, cy] of centers) {
|
|
expect(cx).toBeGreaterThanOrEqual(4);
|
|
expect(cy).toBeGreaterThanOrEqual(4);
|
|
expect(cx).toBeLessThanOrEqual(size - 4);
|
|
expect(cy).toBeLessThanOrEqual(size - 4);
|
|
}
|
|
});
|
|
|
|
it("escapes nothing it shouldn't: the payload never appears in the markup", () => {
|
|
// The QR encodes the URI as modules; the raw string (with its secret) must
|
|
// not leak into the SVG text.
|
|
const svg = renderQrSvg(SAMPLE);
|
|
expect(svg).not.toContain("nostrconnect");
|
|
expect(svg).not.toContain("secret");
|
|
});
|
|
});
|