/** * reach.test.ts — the virtual user boundary, at the passage points. * * A virtual user must simulate the boundary of the future single-user wallet: the * access functions are confined to the user currently connected, and no cross-user * access is permitted. Before this, `docs.sparqlQuery`/`sparqlUpdate` — both * exported from the SDK entry — reached ANY document of ANY identity given a * session id and a NURI. * * The one act that legitimately crosses: DEPOSITING into someone's inbox. It is * how a link travels between users at all, and it gives the depositor nothing back. */ import { test, expect, mock, afterAll } from "bun:test"; import { sparqlQuery, sparqlUpdate } from "../src/surface/docs"; import { depositInto } from "../src/emulated-verifier/register-write"; import { createEntityDoc, resetRegistryCache, userInbox } from "../src/shared-wallet/account-registry"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; import { configure } from "../src/index"; import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap"; import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap"; import { mayReach, mustNotAttempt } from "../src/emulated-verifier/reach"; import { hasReadCap } from "../src/model/nuri"; afterAll(() => { resetConfig(); resetStoreRegistry(); resetCaps(); setCurrentUser(null); }); const SESSION: RegistrySession = { sessionId: "sid-reach", privateStoreId: "PRIV-REACH" }; function inject() { let n = 0; const quads: Array<{ g: string; s: string; p: string; o: string }> = []; const ng = { doc_create: mock(async () => `did:ng:o:reach${++n}`), sparql_update: mock(async (...a: unknown[]) => { quads.push({ g: String(a[2]), s: "", p: "", o: String(a[1]) }); return undefined; }), sparql_query: mock(async () => ({ results: { bindings: [] } })), }; configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim() }); resetRegistryCache(); resetCaps(); setCurrentUser(null); return { ng, quads }; } const READ = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"; test("the guard is inert until the first cap exists (no regression for a cap-free consumer)", async () => { const { ng } = inject(); // Nothing has been created, so no cap has been issued: everything flows. expect(mayReach("did:ng:o:anything")).toBe(true); await sparqlQuery(SESSION.sessionId, READ, undefined, "did:ng:o:anything"); expect(ng.sparql_query).toHaveBeenCalledTimes(1); }); test("once caps exist, a document outside the connected user's reach is refused — read AND write", async () => { inject(); setCurrentUser("alice"); const mine = await createEntityDoc("alice", "private"); // Mine: reachable. expect(mayReach(mine)).toBe(true); await sparqlQuery(SESSION.sessionId, READ, undefined, mine); // A well-formed NURI I hold nothing for: named, unreachable. Both directions. const theirs = "did:ng:o:someone-elses-doc" as const; expect(mayReach(theirs)).toBe(false); await expect(sparqlQuery(SESSION.sessionId, READ, undefined, theirs)).rejects.toThrow( /does not hold this document.s cap/i, ); await expect( sparqlUpdate(SESSION.sessionId, "INSERT DATA { \"c\" }", theirs), ).rejects.toThrow(/does not hold this document.s cap/i); }); test("the boundary follows the connected user — one user's document is another's forbidden NURI", async () => { inject(); setCurrentUser("alice"); const aliceDoc = await createEntityDoc("alice", "private"); setCurrentUser("bob"); const bobDoc = await createEntityDoc("bob", "private"); expect(mayReach(bobDoc)).toBe(true); expect(mayReach(aliceDoc)).toBe(false); // bob is connected await expect(sparqlQuery(SESSION.sessionId, READ, undefined, aliceDoc)).rejects.toThrow(); setCurrentUser("alice"); expect(mayReach(aliceDoc)).toBe(true); expect(mayReach(bobDoc)).toBe(false); }); test("a user reaches its OWN stores and inbox — the boundary must not lock it out of itself", async () => { inject(); setCurrentUser("alice"); await createEntityDoc("alice", "protected"); // provisions alice's account const inbox = await userInbox("alice", "protected"); expect(mayReach(inbox)).toBe(true); await sparqlQuery(SESSION.sessionId, READ, undefined, inbox); // …and not another user's inbox. setCurrentUser("bob"); expect(mayReach(inbox)).toBe(false); }); test("DEPOSITING into another user's inbox crosses the boundary, and gives nothing back", async () => { const { ng } = inject(); setCurrentUser("bob"); const bobInbox = await userInbox("bob", "protected"); setCurrentUser("alice"); await createEntityDoc("alice", "private"); // alice now holds caps → guard is armed expect(mayReach(bobInbox)).toBe(false); // she holds no cap for it // The deposit goes through anyway — it is the one legitimate cross-user act. const before = ng.sparql_update.mock.calls.length; await depositInto(SESSION.sessionId, 'INSERT DATA { "c" }', bobInbox); expect(ng.sparql_update.mock.calls.length).toBe(before + 1); // …and it grants her nothing: she still cannot read that inbox. expect(mayReach(bobInbox)).toBe(false); await expect(sparqlQuery(SESSION.sessionId, READ, undefined, bobInbox)).rejects.toThrow( /does not hold this document.s cap/i, ); }); test("the shim is reached by the MACHINERY, not by an exemption in the boundary", async () => { inject(); setCurrentUser("alice"); await createEntityDoc("alice", "private"); // arms the emulation, resolves the shim // The store-root and the doc-shim are NOT reachable through the virtual-user // surface — there is no exemption list any more. The machinery reaches them // through its own primitives (`physical.ts`), which the boundary never sees and // which are never exported from the package. expect(mayReach(`did:ng:${SESSION.privateStoreId}`)).toBe(false); await expect( sparqlQuery(SESSION.sessionId, READ, undefined, `did:ng:${SESSION.privateStoreId}`), ).rejects.toThrow(/does not hold this document's cap/i); // …yet the registry works, because it never asked through that door. const doc = await createEntityDoc("alice", "protected"); expect(mayReach(doc)).toBe(true); }); // The two rules are deliberately redundant, and this is what that buys. test("rule 1 and rule 2 are independent — the guard still holds if a caller forgets to check", async () => { inject(); setCurrentUser("alice"); await createEntityDoc("alice", "private"); // arms the emulation const theirs = "did:ng:o:not-mine" as const; // RULE 2 — a caller that checks first simply does not issue the operation. expect(mustNotAttempt(theirs)).toBe(true); // RULE 1 — and a caller that does NOT check is refused anyway. This is the whole // point of implementing the same criterion in two places: rule 2 is where the // model lives (you cannot address what you hold no cap for), rule 1 is what makes // a lapse in rule 2 fail loudly instead of quietly succeeding. await expect(sparqlQuery(SESSION.sessionId, READ, undefined, theirs)).rejects.toThrow( /does not hold this document's cap/i, ); }); // Possession decides, not the shape of the reference the caller happens to hold. test("a BARE reference is reachable when the cap is possessed elsewhere", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "private"); // `doc` is the bare form — it carries no cap — yet alice possesses that cap, so // reaching it is legitimate. Manipulating a bare NURI is normal: references travel // bare through content and indexes while the cap sits in what the user holds. expect(hasReadCap(doc)).toBe(false); expect(mayReach(doc)).toBe(true); await sparqlQuery(SESSION.sessionId, READ, undefined, doc); // The cap-bearing form of the same document answers alike. expect(mayReach(`${doc}:r:OK`)).toBe(true); // And bob, holding neither, cannot reach it in either form. setCurrentUser("bob"); expect(mayReach(doc)).toBe(false); expect(mayReach(`${doc}:r:OK`)).toBe(false); }); // The whole point of splitting the machinery out: one API is the app's, the other // must never be. A regression here is silent and total — an app holding the // machinery reaches every virtual user's documents. test("the machinery is NOT part of the package's public surface", async () => { const entry: Record = await import("../src/index"); for (const name of Object.keys(entry)) { expect(name).not.toMatch(/^physical/); } // Named explicitly, so adding one and forgetting the rule fails here. for (const forbidden of ["physicalQuery", "physicalUpdate", "physicalCreate", "subscribePhysicalDoc"]) { expect(entry[forbidden]).toBeUndefined(); } // …and the machinery accessors the merged entry deliberately stopped publishing // (2026-08-07): internal wiring and test resets are reached by their internal path. for (const unpublished of ["getConfig", "getStoreRegistryDeps", "resetConfig", "resetStoreRegistry", "resetCaps", "getCaps", "getCurrentUser"]) { expect(entry[unpublished]).toBeUndefined(); } // The cross-account fan-out is gone from the registry entirely. const registry = entry.storeRegistry as Record; for (const gone of ["listEntityDocs", "resolveReadGraphs", "allAccounts", "loadShim"]) { expect(registry[gone]).toBeUndefined(); } });