// node --test TheseusNavigator/dev/theseus-id.test.cjs // lib/theseus-id.cjs with a fake vault, fake prompts and fake fetchers. "use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { pathToFileURL } = require("node:url"); const { createTheseusIdHost, SILENT_PER_MIN } = require("../lib/theseus-id.cjs"); const LIB = pathToFileURL(path.join(__dirname, "..", "..", "TheseusID", "lib", "index.mjs")).href; async function loadLib() { const lib = await import(LIB); const { secp256k1 } = await import("@noble/curves/secp256k1.js"); const { sha256 } = await import("@noble/hashes/sha2.js"); const { ripemd160 } = await import("@noble/hashes/legacy.js"); const { hkdf } = await import("@noble/hashes/hkdf.js"); return { lib, crypto: lib.createCrypto({ secp256k1, sha256, ripemd160, hkdf }) }; } const REGISTRY = { v: 1, seq: 1, projects: [ { project: "hephaestus", name: "Hephaestus", origins: ["https://code.silentmode.st"], provider: { issuer: "https://accounts.silentmode.st", client_id: "hephaestus" }, supports: ["move"] }, { project: "sirius", name: "Sirius", origins: ["https://sirius.x"] }, ], }; function setup({ unlocked = true, confirm, docs = {} } = {}) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tid-")); const vault = { root: unlocked ? new Uint8Array(32).fill(5) : null }; const prompts = []; const host = createTheseusIdHost({ file: path.join(dir, "theseus-id.json"), loadLib, getPurposeRoot: () => vault.root, hasVault: () => true, bundledRegistry: REGISTRY, fetchOriginDoc: async (authority) => { if (!docs[authority]) throw new Error("404"); return docs[authority]; }, ui: { unlock: async () => { vault.root = new Uint8Array(32).fill(5); return true; }, confirm: async (req) => { prompts.push(req); return confirm ? confirm(req) : { ok: true }; }, }, }); return { host, vault, prompts, dir }; } const req = (over = {}) => ({ projectId: "hephaestus", nonce: "nonce-0123456789abcdef", origin: "https://code.silentmode.st", uri: "https://code.silentmode.st/login", gesture: true, ...over }); test("first sign-in prompts, later first-party sign-ins are silent", async () => { const { host, prompts } = setup(); const a = await host.signIn(req()); assert.equal(prompts.length, 1); assert.equal(prompts[0].first, true); assert.match(a.account, /^tid:/); const { lib, crypto } = await loadLib(); assert.ok(crypto.verifyAddress(a.message, a.signature, a.account)); assert.equal(lib.parseMessage(a.message).origin, "https://code.silentmode.st"); const b = await host.signIn(req({ nonce: "nonce-0123456789abcdeg" })); assert.equal(prompts.length, 1, "no second prompt"); assert.equal(b.account, a.account, "same ID every time"); }); test("origins outside the list are refused without any prompt", async () => { const { host, prompts } = setup(); await assert.rejects(host.signIn(req({ origin: "https://evil.example" })), { code: "origin-not-listed" }); await assert.rejects(host.signIn(req({ origin: "https://navigate.st" })), { code: "origin-not-listed" }); await assert.rejects(host.signIn(req({ projectId: "unknown" })), { code: "origin-not-listed" }); assert.equal(prompts.length, 0); // The listed provider may ask for Hephaestus's signature. const viaProvider = await host.signIn(req({ origin: "https://accounts.silentmode.st" })); assert.match(viaProvider.message, /Origin: https:\/\/accounts\.silentmode\.st/); }); test("per-project IDs differ; One ID is shared", async () => { const { host } = setup({ confirm: (r) => ({ ok: true, mode: r.projectId === "sirius" ? "one" : "project" }) }); const h = await host.signIn(req()); const s = await host.signIn(req({ projectId: "sirius", origin: "https://sirius.x", uri: "https://sirius.x/" })); assert.notEqual(h.account, s.account); const ov = await host.overview(); assert.equal(ov.oneId, s.account, "sirius chose One ID"); assert.deepEqual(ov.projects.map((p) => p.mode).sort(), ["one", "project"]); }); test("locked vault: no gesture, no prompt; with a gesture, unlock first", async () => { const { host, vault } = setup({ unlocked: false }); await assert.rejects(host.signIn(req({ gesture: false })), { code: "locked" }); assert.equal(vault.root, null); const r = await host.signIn(req()); assert.match(r.account, /^tid:/); }); test("declining, busy and the silent-rate limit", async () => { const { host, prompts } = setup({ confirm: () => ({ ok: false }) }); await assert.rejects(host.signIn(req()), { code: "denied" }); const s2 = setup(); await s2.host.signIn(req()); const both = await Promise.allSettled([s2.host.signIn(req({ nonce: "nonce-aaaaaaaaaaaaaaaa" })), s2.host.signIn(req({ nonce: "nonce-bbbbbbbbbbbbbbbb" }))]); assert.ok(both.some((x) => x.status === "rejected" && x.reason.code === "busy")); for (let i = 0; i < SILENT_PER_MIN + 2; i++) await s2.host.signIn(req({ nonce: "nonce-cccccccccccccc" + String(i).padStart(2, "0") })); assert.ok(s2.prompts.length >= 2, "beyond the limit, sign-ins prompt again"); }); test("a mode switch never changes the ID silently: move proof, then finish", async () => { const { host } = setup(); const first = await host.signIn(req()); await host.setDefaultMode("one"); const still = await host.signIn(req({ nonce: "nonce-dddddddddddddddd" })); assert.equal(still.account, first.account, "default mode change leaves existing projects alone"); await host.requestMove("hephaestus", { mode: "one" }); const moving = await host.signIn(req({ nonce: "nonce-eeeeeeeeeeeeeeee" })); assert.notEqual(moving.account, first.account); assert.equal(moving.move.from, first.account); const { lib, crypto } = await loadLib(); const v = lib.createVerifier({ crypto, projectId: "hephaestus", origins: ["https://code.silentmode.st"], consumeNonce: () => true }); const ok = await v.verifySignIn({ message: moving.message, signature: moving.signature, move: moving.move }); assert.equal(ok.movedFrom, first.account); await host.moved({ projectId: "hephaestus", origin: "https://code.silentmode.st", account: moving.account }); const after = await host.signIn(req({ nonce: "nonce-ffffffffffffffff" })); assert.equal(after.account, moving.account); assert.equal(after.move, undefined); // Sirius does not list "move": refused. await host.signIn(req({ projectId: "sirius", origin: "https://sirius.x" })); await assert.rejects(host.requestMove("sirius", { mode: "one" }), { code: "move-unsupported" }); }); test("state is encrypted, survives a restart, and is unreadable with another vault", async () => { const s = setup(); const a = await s.host.signIn(req()); const raw = fs.readFileSync(path.join(s.dir, "theseus-id.json"), "utf8"); assert.ok(!raw.includes("hephaestus") && !raw.includes(a.account), "no plain project names or IDs on disk"); s.host.forget(); const ov = await s.host.overview(); assert.equal(ov.projects[0].account, a.account); s.vault.root = new Uint8Array(32).fill(9); s.host.forget(); assert.deepEqual((await s.host.overview()).projects, []); }); test("name-scoped projects fetch their list; BNS lists must be owner-signed", async () => { const { lib, crypto } = await loadLib(); const ownerKey = new Uint8Array(32).fill(3); const owner = crypto.account(ownerKey, "bitcoincash"); const body = { v: 1, project: "game.x", origins: ["https://game.x"] }; const signed = { ...body, sig: crypto.sign(ownerKey, lib.canonicalJson(body)) }; const { host } = setup({ docs: { "game.x": { doc: signed, bns: true, owner }, "blog.example.org": { doc: { v: 1, project: "sirius-press:blog.example.org", origins: ["https://blog.example.org"] }, bns: false }, "bad.x": { doc: body, bns: true, owner }, } }); assert.match((await host.signIn(req({ projectId: "game.x", origin: "https://game.x" }))).account, /^tid:/); assert.match((await host.signIn(req({ projectId: "sirius-press:blog.example.org", origin: "https://blog.example.org" }))).account, /^tid:/); await assert.rejects(host.signIn(req({ projectId: "bad.x", origin: "https://bad.x" })), { code: "origin-list-unavailable" }); });