/** * ReadCap ACTIVE (T03.h) — end-to-end proof that the emulated SDK enforces * per-DOCUMENT isolation, driven by per-entity documents + BILATERAL connections. * * Mirrors exactly what the app does: create an entity document through the REAL * registry (`createEntityDoc`), declare its cap policy via * `getCaps().open(doc, scope, owner)`, set the current identity, and declare * connections as the CURRENT identity's own peers (authenticated, bilateral). The * read filter then discriminates: * (a) unconnected principal denied a PROTECTED doc; granted after a BILATERAL * connection; PUBLIC readable throughout — via the ACTIVE ReadCap. * (b) a UNILATERAL / self-declared connection grants NOTHING. */ import { test, expect, mock, afterAll } from "bun:test"; import { createEntityDoc, resetRegistryCache } from "../src/store-registry"; import type { RegistrySession } from "../src/store-registry"; import { configure, configureStoreRegistry, resetStoreRegistry, resetConfig, getCaps, resetCaps, setCurrentUser, declareConnections, } from "../src/polyfill"; import { filterReadable } from "../src/read-filter"; afterAll(() => { resetConfig(); resetStoreRegistry(); resetCaps(); setCurrentUser(null); }); const SESSION: RegistrySession = { sessionId: "sid", privateStoreId: "PRIV" }; function inject() { let n = 0; const ng = { doc_create: mock(async () => `did:ng:o:doc${++n}`), sparql_update: mock(async () => undefined), sparql_query: mock(async () => ({ results: { bindings: [] } })), }; configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeUser: (u) => u.trim() }); resetRegistryCache(); resetCaps(); setCurrentUser(null); return ng; } test("ReadCap active: a private entity doc created via the real registry is hidden from another principal", async () => { inject(); const aliceDoc = await createEntityDoc("alice", "private"); getCaps().open(aliceDoc, "private", "alice"); const bobDoc = await createEntityDoc("bob", "public"); getCaps().open(bobDoc, "public", "bob"); const items = [ { "@graph": aliceDoc, "@id": "a1", label: "alice-private" }, { "@graph": bobDoc, "@id": "b1", label: "bob-public" }, ]; expect(filterReadable(items, getCaps(), "bob").map((i) => i["@id"])).toEqual(["b1"]); expect(filterReadable(items, getCaps(), "alice").map((i) => i["@id"]).sort()).toEqual(["a1", "b1"]); expect(filterReadable(items, getCaps(), null).map((i) => i["@id"])).toEqual(["b1"]); expect(getCaps().hasReadPolicy()).toBe(true); }); // (a) protected hidden while unconnected → revealed after a BILATERAL connection; // public readable regardless — all through the ACTIVE ReadCap. test("(a) PROTECTED doc: hidden unconnected, revealed after BILATERAL connection, PUBLIC always readable", async () => { inject(); const aliceProtected = await createEntityDoc("alice", "protected"); getCaps().open(aliceProtected, "protected", "alice"); const alicePublic = await createEntityDoc("alice", "public"); getCaps().open(alicePublic, "public", "alice"); const items = [ { "@graph": aliceProtected, "@id": "p1" }, { "@graph": alicePublic, "@id": "u1" }, ]; const view = (u: string) => filterReadable(items, getCaps(), u).map((i) => i["@id"]).sort(); // BEFORE any connection: bob sees only the public item. expect(view("bob")).toEqual(["u1"]); expect(view("alice")).toEqual(["p1", "u1"]); // BILATERAL: alice asserts bob AND bob asserts alice → the link materializes and // the SDK issues the protected doc's read cap to bob. declareConnections(["bob"], "alice"); declareConnections(["alice"], "bob"); expect(view("bob")).toEqual(["p1", "u1"]); // A third, unconnected principal still sees only the public one. expect(view("carol")).toEqual(["u1"]); }); // (b) A UNILATERAL / self-declared connection must NOT grant protected read. test("(b) a UNILATERAL / self-declared connection grants NO protected read", async () => { inject(); const aliceProtected = await createEntityDoc("alice", "protected"); getCaps().open(aliceProtected, "protected", "alice"); const items = [{ "@graph": aliceProtected, "@id": "p1" }]; const view = (u: string) => filterReadable(items, getCaps(), u).map((i) => i["@id"]); // The ATTACKER (mallory) self-declares a connection to alice — a UNILATERAL // assertion authored by mallory. Alice NEVER asserts mallory back. declareConnections(["alice"], "mallory"); expect(view("mallory")).toEqual([]); // still denied — no bilateral link // Even if alice connects to bob (a different, legitimate bilateral link), // mallory's one-sided assertion still grants nothing. declareConnections(["bob"], "alice"); declareConnections(["alice"], "bob"); expect(view("mallory")).toEqual([]); expect(view("bob")).toEqual(["p1"]); });