/** * ReadCap ACTIVE — end-to-end proof that the emulated SDK enforces per-DOCUMENT * isolation, driven by per-entity documents + DIRECTED read grants. * * Mirrors 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 — when the app decides two identities * are related — issue a DIRECTED read grant on each of the owner's protected * documents (`getCaps().grantRead(doc, granteeId)`). Whether identities are * "connected" is the application's own concept: this test plays that role * directly. The read filter then discriminates: * (a) an ungranted principal is denied a PROTECTED doc; granted once the owner * issues a directed grant; PUBLIC readable throughout — via the ACTIVE * ReadCap. * (b) no grant → no protected read (a reader cannot grant itself). */ 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, } 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 }); // Synchronous fake store → no sync lag; disable the anti-fork retry backoff. configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim(), provisionRetry: { attempts: 1 } }); resetRegistryCache(); resetCaps(); setCurrentUser(null); return ng; } /** The app's relationship concept, played inline: grant `reader` the read cap of * every protected document owned by `owner`. */ function grantOwnerProtectedTo(owner: string, reader: string) { for (const doc of getCaps().protectedDocsOf(owner)) getCaps().grantRead(doc, reader); } 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 ungranted → revealed after a DIRECTED grant; public // readable regardless — all through the ACTIVE ReadCap. test("(a) PROTECTED doc: hidden ungranted, revealed after a DIRECTED grant, 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 grant: bob sees only the public item. expect(view("bob")).toEqual(["u1"]); expect(view("alice")).toEqual(["p1", "u1"]); // The app decides alice↔bob are related and grants bob the read cap of alice's // protected documents. grantOwnerProtectedTo("alice", "bob"); expect(view("bob")).toEqual(["p1", "u1"]); // A third, ungranted principal still sees only the public one. expect(view("carol")).toEqual(["u1"]); }); // (b) An identity gets no protected read until the OWNER issues the grant — a // reader cannot grant itself. test("(b) no directed grant → 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"]); // mallory holds no grant on alice's protected doc → denied. expect(view("mallory")).toEqual([]); // Granting bob (a different, legitimate reader) leaves mallory denied. grantOwnerProtectedTo("alice", "bob"); expect(view("mallory")).toEqual([]); expect(view("bob")).toEqual(["p1"]); });