/** * The access gate's identity resolution. * * This is the piece whose failure is SILENT: get the order wrong and the broker iframe * reads an empty identity, provisions a second virtual user, and the returning user * lands in an empty space with no error anywhere. So the order is pinned, not trusted. */ import { getCurrentUser } from "../src/shared-wallet/bootstrap"; import { test, expect, afterEach } from "bun:test"; import { configure } from "../src/index"; import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap"; import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap"; import { ensureIdentity } from "../src/shared-wallet/access-gate"; const KEY = "ng-eventually:identity"; /** A localStorage double — the real one is absent in `bun test`. */ function fakeStorage(initial: Record = {}) { const map = new Map(Object.entries(initial)); return { getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => void map.set(k, v), removeItem: (k: string) => void map.delete(k), get size() { return map.size; }, }; } /** Put the page in a given URL + storage state, as the browser would. */ function inPage(search: string, storage: ReturnType) { (globalThis as any).location = { search, href: "https://app.example" + search }; (globalThis as any).localStorage = storage; (globalThis as any).history = { replaceState: () => {} }; } afterEach(() => { setCurrentUser(null); resetConfig(); resetStoreRegistry(); delete (globalThis as any).location; delete (globalThis as any).localStorage; delete (globalThis as any).history; }); function configured() { configureStoreRegistry({ getSession: async () => ({ sessionId: "s", privateStoreId: "did:ng:o:p" }), normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(), }); configure({ ng: {} as never, useShape: (() => {}) as never, sharedWallet: { fileUrl: "/w.ngw", password: "pw" }, }); } test("an identity already set is left alone — the gate never re-asks", async () => { configured(); inPage("", fakeStorage()); setCurrentUser("alice"); await ensureIdentity(); expect(getCurrentUser()).toBe("alice"); }); test("the URL parameter WINS over storage — it is the only thing that crosses the frontier", async () => { // The top-level page and the broker iframe have separate localStorage partitions, so a // value written on one side is not the value the other reads. The URL survives the // round-trip; storage does not. If storage won here, a user entering a second // identifier would keep being sent back to the first one's space. configured(); inPage("?ng-id=fromurl", fakeStorage({ [KEY]: "fromstorage" })); await ensureIdentity(); expect(getCurrentUser()).toBe("fromurl"); }); test("the URL parameter is copied into THIS partition, so a plain reload still knows", async () => { configured(); const storage = fakeStorage(); inPage("?ng-id=carol", storage); await ensureIdentity(); expect(storage.getItem(KEY)).toBe("carol"); }); test("with no parameter, storage answers — a reload does not re-ask", async () => { configured(); inPage("", fakeStorage({ [KEY]: "dana" })); await ensureIdentity(); expect(getCurrentUser()).toBe("dana"); }); test("nothing known and no DOM to ask on → it refuses loudly", async () => { // Continuing silently would provision an anonymous virtual space, which is the failure // this module exists to prevent. The error names what the caller must do. configured(); inPage("", fakeStorage()); await expect(ensureIdentity()).rejects.toThrow(/no DOM to ask on/i); }); test("no shared wallet configured → it refuses, rather than inventing a space", async () => { configure({ ng: {} as never, useShape: (() => {}) as never }); inPage("", fakeStorage()); await expect(ensureIdentity()).rejects.toThrow(/no shared wallet configured/i); }); test("the URL value is NORMALIZED on the way in — `@Erin` and `erin` are one space", async () => { // Ported from the consumer's `identifiant-resolution` feature, and it caught a real // defect here: the gate normalized what a user TYPED but not what the URL carried, so // a link with `?ng-id=@Erin` keyed onto a different virtual user than the same person // typing `erin`. One normalizer — the injected one — for all three entry paths. configured(); inPage("?ng-id=@Erin", fakeStorage()); await ensureIdentity(); expect(getCurrentUser()).toBe("erin"); }); test("a stored value is normalized too — an old entry cannot key onto a second space", async () => { configured(); inPage("", fakeStorage({ [KEY]: "@Frank" })); await ensureIdentity(); expect(getCurrentUser()).toBe("frank"); });