/** * Cross-user access — the scenario that proves the model end to end. * * Alice owns a PROTECTED document and a PUBLIC one, and the public one carries a * REFERENCE to the protected one. Then: * * - **Bob** has the public document's link. He reads it, sees the reference, and * cannot read what it points at. Naming is not reading, and publication is * **not recursive**: a public object may point at private content without * disclosing it. * - **Charlie** has the public document's link AND was given the protected * document's cap. Same reference, same path — he reads through it. * - **Bob, dynamically**: Alice delivers the cap to Bob's inbox. Processing the * inbox files it, which fires the held-caps signal, which re-runs the read — the * protected document appears with nothing else happening. * * The difference between Bob and Charlie is ONLY each of them holds. There is * no authorization list anywhere, and nobody was named to the registry. */ import { getCaps } from "../src/shared-wallet/bootstrap"; import { test, expect, mock, afterAll } from "bun:test"; import { createEntityDoc, resetRegistryCache, userInbox, } from "../src/shared-wallet/account-registry"; import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; import { configure, configureStoreRegistry, connectedUser, setCurrentUser } from "../src/index"; import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap"; import { share } from "../src/surface/inbox"; import { post, postToDocument, read as readInbox } from "../src/surface/inbox"; import { readUnion } from "../src/surface/read-model"; import { sparqlUpdate } from "../src/surface/docs"; import type { Nuri } from "../src/model/types"; /** * Do I hold this document's cap? Possession, asked of the internal registry — the * polyfill door stopped publishing this (see `polyfill.ts`), because as an app-facing * question it reads like "may I read this?" and a public store's document answers * `false` until something has asked for its cap. */ function hasCap(nuri: Nuri): boolean { return getCaps().capFor(nuri) !== undefined; } afterAll(() => { resetConfig(); resetStoreRegistry(); resetCaps(); setCurrentUser(null); }); const SESSION: RegistrySession = { sessionId: "sid-x", privateStoreId: "PRIV-X" }; const SHIM = "urn:ng-eventually:shim"; const INBOX = "urn:ng-eventually:inbox"; /** The predicate Alice uses to point from her public doc at her protected one. */ const REFERS_TO = "urn:e2e:refersTo"; const SECRET = "urn:e2e:secret"; interface Quad { g: string; s: string; p: string; o: string } function unescapeLiteral(s: string): string { let out = ""; for (let i = 0; i < s.length; i++) { if (s[i] === "\\" && i + 1 < s.length) { const next = s[++i]; out += next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? "\t" : next!; } else out += s[i]; } return out; } /** A stateful fake `ng`: the shim SPARQL, the inbox SPARQL, and the anchored * per-doc `?s ?p ?o` read the read-model uses. */ function makeFakeNg() { const quads: Quad[] = []; let docCounter = 0; const doc_create = mock(async () => `did:ng:o:doc${++docCounter}`); const sparql_update = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[2] as string | undefined; if (!anchor) return undefined; // `DELETE WHERE {

?var }` — the form the lib uses to REPLACE a value // (see docs/decisions/sparql-delete-for-orm-objects.md). Without this arm the // fake would treat the delete as an insert and the replacement would silently // become an accumulation — the exact bug a replacement exists to prevent. const del = query.match(/^\s*DELETE\s+WHERE\s*\{\s*<([^>]+)>\s+<([^>]+)>\s+\?/); if (del) { const [s0, p0] = [del[1]!, del[2]!]; for (let i = quads.length - 1; i >= 0; i--) { const q = quads[i]!; if (q.g === anchor && q.s === s0 && q.p === p0) quads.splice(i, 1); } return undefined; } const body = query.replace(/^\s*INSERT DATA\s*\{/, "").replace(/\}\s*$/, ""); const sm = body.match(/<([^>]+)>/); if (!sm) return undefined; const s = sm[1]!; const pairRe = /(?:a|<([^>]+)>)\s+(?:"((?:[^"\\]|\\.)*)"|<([^>]+)>)/g; let m: RegExpExecArray | null; const after = body.slice(body.indexOf(sm[0]) + sm[0].length); while ((m = pairRe.exec(after)) !== null) { const p = m[1] ?? (query.includes(`${INBOX}:Deposit`) ? `${INBOX}:Deposit` : `${SHIM}:Account`); const o = m[2] !== undefined ? unescapeLiteral(m[2]) : (m[3] ?? ""); quads.push({ g: anchor, s, p, o }); } return undefined; }); const sparql_query = mock(async (...a: unknown[]) => { const query = a[1] as string; const anchor = a[3] as string | undefined; if (query.includes(`<${SHIM}:shimDoc>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:shimDoc`).map((q) => ({ shimDoc: { value: q.o } })) } }; } if (query.includes(`<${SHIM}:id>`)) { const subjM = query.match(/<([^>]+)>\s+a\s+/); const only = subjM ? subjM[1]! : null; const bySubject = new Map>(); for (const q of quads) { if (q.g !== anchor) continue; if (only !== null && q.s !== only) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === `${SHIM}:id`) rec.id = q.o; if (q.p === `${SHIM}:docPublic`) rec.docPublic = q.o; if (q.p === `${SHIM}:docProtected`) rec.docProtected = q.o; if (q.p === `${SHIM}:docPrivate`) rec.docPrivate = q.o; bySubject.set(q.s, rec); } return { results: { bindings: [...bySubject.values()].filter((r) => r.id).map((r) => ({ id: { value: r.id! }, docPublic: { value: r.docPublic ?? "" }, docProtected: { value: r.docProtected ?? "" }, docPrivate: { value: r.docPrivate ?? "" }, })), }, }; } if (query.includes(`<${INBOX}:payload>`)) { const bySubject = new Map>(); for (const q of quads) { if (q.g !== anchor) continue; const rec = bySubject.get(q.s) ?? {}; if (q.p === `${INBOX}:payload`) rec.payload = q.o; if (q.p === `${INBOX}:ts`) rec.ts = q.o; if (q.p === `${INBOX}:from`) rec.from = q.o; bySubject.set(q.s, rec); } return { results: { bindings: [...bySubject.values()] .filter((r) => r.payload !== undefined && r.ts !== undefined) .map((r) => { const row: Record = { payload: { value: r.payload! }, ts: { value: r.ts! } }; if (r.from !== undefined) row.from = { value: r.from }; return row; }), }, }; } // User-branch `link` SELECT (the emulated AddLink records). // User-branch `inboxCap` SELECT (the emulated AddInboxCap records). if (query.includes(`<${SHIM}:inboxCap>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:inboxCap`).map((q) => ({ c: { value: q.o } })) } }; } // Header-branch `inboxAddress` SELECT (where to deposit for this document). if (query.includes(`<${SHIM}:inboxAddress>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:inboxAddress`).map((q) => ({ a: { value: q.o } })) } }; } // Store-branch `readCap` SELECT (the emulated AddRepo records). if (query.includes(`<${SHIM}:readCap>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:readCap`).map((q) => ({ c: { value: q.o } })) } }; } if (query.includes(`<${SHIM}:link>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:link`).map((q) => ({ c: { value: q.o } })) } }; } // Header-branch `exposedReadCap` SELECT — what a PUBLIC store serves to anyone. if (query.includes(`<${SHIM}:exposedReadCap>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:exposedReadCap`).map((q) => ({ c: { value: q.o } })) } }; } if (query.includes(`<${SHIM}:contains>`)) { return { results: { bindings: quads.filter((q) => q.g === anchor && q.p === `${SHIM}:contains`).map((q) => ({ e: { value: q.o } })) } }; } // Anchored per-doc read (readUnion `SELECT ?s ?p ?o`) — the document's content. return { results: { bindings: quads .filter((q) => q.g === anchor) .map((q) => ({ s: { value: q.s }, p: { value: q.p }, o: { value: q.o } })), }, }; }); return { doc_create, sparql_update, sparql_query, _quads: quads }; } function inject() { const ng = makeFakeNg(); configure({ ng: ng as any, useShape: (() => {}) as any }); configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim().toLowerCase() }); resetRegistryCache(); resetCaps(); setCurrentUser(null); return ng; } /** Write one triple into `doc`, as the consumer's write path would. */ async function write(doc: Nuri, p: string, o: string): Promise { await sparqlUpdate(SESSION.sessionId, `INSERT DATA { <${doc}> <${p}> "${o}" }`, doc, "test"); } /** The values `p` carries in the documents `docs`, as the current holder reads them. */ async function readValues(docs: Nuri[], p: string): Promise { const subjects = await readUnion(docs); return subjects.flatMap((s) => s.props[p] ?? []); } /** * Alice's world: a protected document holding a secret, and a public document that * REFERS to it by bare NURI. * * What crosses to the other actors is **the bare reference of the public document and * nothing else** — no cap, no link with a key in it. That is the whole discipline of * this file: an application circulates references, and if a test had to hand a key * across an identity boundary through a JS variable, the feature it claims to prove * would have no path in any real application. */ async function aliceSetsUpHerDocuments() { setCurrentUser("alice"); const protDoc = await createEntityDoc("alice", "protected"); await write(protDoc, SECRET, "the-protected-content"); const pubDoc = await createEntityDoc("alice", "public"); // The reference is the BARE NURI of the protected document: it names it, and // grants nothing. This is the whole point of the scenario. await write(pubDoc, REFERS_TO, protDoc); return { protDoc, pubDoc }; } /** Follow the reference found in the public document — what a reader actually does. */ function referenceFoundIn(values: string[]): Nuri { const ref = values[0]; expect(ref).toBeDefined(); return ref as Nuri; } test("Bob: reads the public document, sees the reference, and cannot read through it", async () => { inject(); const { protDoc, pubDoc } = await aliceSetsUpHerDocuments(); setCurrentUser("bob"); // Bob holds the BARE reference and nothing else. The document sits in a public // store, so the store serves him its cap — he never received a key from anyone. // He reads the public document and finds the reference. const refs = await readValues([pubDoc], REFERS_TO); const ref = referenceFoundIn(refs); expect(ref).toBe(protDoc); // he can NAME Alice's protected document // …and that is all it gets him: no cap, no read. Publication is NOT recursive. expect(hasCap(ref)).toBe(false); expect(await readValues([ref], SECRET)).toEqual([]); }); test("Charlie: same public document, same reference — and he reads through it", async () => { inject(); const { protDoc, pubDoc } = await aliceSetsUpHerDocuments(); const CHARLIE_INBOX = await userInbox("charlie", "protected"); // Alice decides Charlie may read that ONE document, and delivers its cap to his // inbox. She names no principal to the registry; she addresses an inbox. setCurrentUser("alice"); await share(protDoc, "charlie"); setCurrentUser("charlie"); await readInbox(CHARLIE_INBOX); // processing the inbox files the cap const ref = referenceFoundIn(await readValues([pubDoc], REFERS_TO)); expect(ref).toBe(protDoc); expect(hasCap(ref)).toBe(true); expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]); }); test("the ONLY difference between Bob and Charlie is each of them holds", async () => { inject(); const { protDoc } = await aliceSetsUpHerDocuments(); const CHARLIE_INBOX = await userInbox("charlie", "protected"); setCurrentUser("alice"); await share(protDoc, "charlie"); setCurrentUser("bob"); const bobSees = await readValues([protDoc], SECRET); setCurrentUser("charlie"); await readInbox(CHARLIE_INBOX); const charlieSees = await readValues([protDoc], SECRET); expect(bobSees).toEqual([]); expect(charlieSees).toEqual(["the-protected-content"]); }); // The dynamic version: Bob is refused, then the cap lands in his inbox and the read // that was empty becomes full — with nothing re-declared and nobody re-authorized. test("dynamic: a cap delivered to Bob's inbox makes the refused document readable, and signals it", async () => { inject(); const { protDoc, pubDoc } = await aliceSetsUpHerDocuments(); const BOB_INBOX = await userInbox("bob", "protected"); setCurrentUser("bob"); const ref = referenceFoundIn(await readValues([pubDoc], REFERS_TO)); // Before: named, unreadable. expect(await readValues([ref], SECRET)).toEqual([]); // A reader that re-reads whenever what it holds changes — this is exactly what // `watchShape` wires internally, played here on an ad-hoc read. let reread = 0; let latest: string[] = []; const unsub = getCaps().onChange(() => { reread += 1; void readValues([ref], SECRET).then((v) => (latest = v)); }); // Alice delivers the cap. Bob's client processes his inbox — the only thing that // happens; no "receive" call exists. setCurrentUser("alice"); await share(protDoc, "bob"); setCurrentUser("bob"); await readInbox(BOB_INBOX); // Filing the cap fired the signal… expect(reread).toBeGreaterThan(0); await Promise.resolve(); await new Promise((r) => setTimeout(r, 0)); // …and the read that was empty now yields the content. expect(hasCap(ref)).toBe(true); expect(latest).toEqual(["the-protected-content"]); expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]); unsub(); }); // The property this whole batch exists for, stated on its own: WHERE a document sits // decides whether a bare reference is enough. Upstream a public store's repos are // served on the outer overlay and their ReadCap is downloaded from it // (`PublicRepoLinkV0`, `engine/net/src/types.rs:5098`) — so the same value transmitted // (a bare reference) yields a different outcome depending on the store, and never // because a key travelled. test("a bare reference is enough for a PUBLIC document, and not for a protected one", async () => { inject(); const { protDoc, pubDoc } = await aliceSetsUpHerDocuments(); setCurrentUser("bob"); // Bob has been given nothing but the two NURIs. expect((await readValues([pubDoc], REFERS_TO)).length).toBe(1); expect(await readValues([protDoc], SECRET)).toEqual([]); // And what he obtained for the public one is a READ grant, not a write right: a // public store serves its read cap, no store hands out the write cap. await expect(write(pubDoc, SECRET, "bob-was-here")).rejects.toThrow(/public store/i); }); // THE POINT OF THE LINK: a cap survives because it was APPLIED, not because the // inbox is re-read. Upstream, processing an inbox message files it — `AddLink // { read_cap }` on the User branch of the private store — and the queue is consumed. // Re-reading a queue to recover state is using it as a database. test("a Link is APPLIED durably: the cap survives with the inbox emptied", async () => { const ng = inject(); const { protDoc } = await aliceSetsUpHerDocuments(); const bobInbox = await userInbox("bob", "protected"); setCurrentUser("alice"); await share(protDoc, "bob"); // Bob connects: the library restores + drains, with nothing asked of the app. setCurrentUser("bob"); await connectedUser(); expect(await readValues([protDoc], SECRET)).toEqual(["the-protected-content"]); // Now EMPTY the inbox — as a consumed queue would be — and drop every in-memory // cap, then re-arm the emulation so the boundary is actually in force again. for (let k = ng._quads.length - 1; k >= 0; k--) { if (ng._quads[k]!.g === bobInbox) ng._quads.splice(k, 1); } resetCaps(); setCurrentUser("alice"); await createEntityDoc("alice", "private"); // re-arms: a cap exists again setCurrentUser("bob"); // Checked SYNCHRONOUSLY, before yielding: `setCurrentUser` fires the connection work // itself, and that work is precisely what restores the cap. An awaited check here // would be asserting who won a race, not what the library does. expect(hasCap(protDoc)).toBe(false); // bob holds nothing yet // Connecting restores it — from the User branch, since the inbox has nothing left. await connectedUser(); expect(hasCap(protDoc)).toBe(true); expect(await readValues([protDoc], SECRET)).toEqual(["the-protected-content"]); }); test("connecting a user that does not exist provisions nothing", async () => { inject(); setCurrentUser("nobody"); await connectedUser(); // No account, no stores, no caps — connecting must not create a user as a side // effect, or the emulation would arm itself in the background. expect(getCaps().isEnforcing()).toBe(false); }); // PER-DOCUMENT INBOXES. Upstream a repo carries `inbox: Option` and its // owner records the private half with `AddInboxCap` on the User branch — the same // branch as `AddLink`. So "which inboxes may I read" has one answer, and connecting // drains them all: the user's own, and one per document it opened an inbox on. test("a document has its own inbox: anyone deposits, only the owner reads", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "public"); const aliceInbox = await openDocumentInbox(doc); expect(aliceInbox).not.toBe(await userInbox("alice", "protected")); // Bob RESOLVES the address himself, from the BARE reference — the only thing he is // handed, and the only thing an application circulates. The document is in a public // store, so the store serves him its read cap; the address is not passed to him, // because if it had to be there would be no way for an app to get it. setCurrentUser("bob"); const bobTarget = await documentInboxAddress(doc); expect(bobTarget).toBe(aliceInbox); // …and it is the SAME inbox alice reads await post(bobTarget!, { payload: { joining: true }, ts: 1 }); // …and he cannot read it back: depositing grants nothing. await expect(readInbox(bobTarget!)).rejects.toThrow(/does not belong to the connected wallet/i); // Alice reads her document's inbox, because she opened it. setCurrentUser("alice"); const deposits = await readInbox(aliceInbox); expect(deposits.map((d) => d.payload)).toEqual([{ joining: true }]); }); test("opening an inbox on someone else's document is refused, not silently forked", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "public"); const aliceInbox = await openDocumentInbox(doc); // Bob can READ the document (it is in a public store) — and reading is not ownership. setCurrentUser("bob"); await expect(openDocumentInbox(doc)).rejects.toThrow(/already has an inbox|you may only open an inbox/i); // The address he resolves is still alice's, so his deposits reach her. expect(await documentInboxAddress(doc)).toBe(aliceInbox); }); test("a fresh document has NO inbox — one belongs to one document, and only its owner opens it", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "public"); // Not "the owner's inbox by default": upstream an inbox belongs to exactly ONE repo // (the verifier routes by `inboxes: PubKey → RepoId`), so pointing several documents // at one inbox is a relation the model cannot express. setCurrentUser("bob"); expect(await documentInboxAddress(doc)).toBeUndefined(); // …and depositing THROWS rather than vanishing — a lost deposit is the bug this // whole path exists to close. await expect(postToDocument(doc, { payload: { x: 1 } })).rejects.toThrow(/has no inbox/i); }); test("opening an inbox publishes ONE address, and re-opening does not accumulate", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "public"); const dedicated = await openDocumentInbox(doc); expect(await openDocumentInbox(doc)).toBe(dedicated); // idempotent setCurrentUser("bob"); expect(await documentInboxAddress(doc)).toBe(dedicated); // The deposit reaches the owner, addressed by the document alone. await postToDocument(doc, { payload: { signingUp: true } }); setCurrentUser("alice"); expect((await readInbox(dedicated)).map((d) => d.payload)).toEqual([{ signingUp: true }]); }); test("the inbox address is machinery: it never surfaces as the document's data", async () => { inject(); setCurrentUser("alice"); const doc = await createEntityDoc("alice", "public"); await write(doc, SECRET, "s1"); await openDocumentInbox(doc); // The consumer read returns the entity's properties and nothing of the compartment // that carries the address — the Header branch is beside the content, not in it. const subjects = await readUnion([doc]); const props = subjects[0]?.props ?? {}; expect(Object.keys(props)).toEqual([SECRET]); }); test("connecting drains BOTH levels: the user's inbox and its documents'", async () => { inject(); setCurrentUser("alice"); const protDoc = await createEntityDoc("alice", "protected"); const pubDoc = await createEntityDoc("alice", "public"); const docInbox = await openDocumentInbox(pubDoc); const aliceInbox = await userInbox("alice", "protected"); // Two deposits, one at each level, both made by someone else. setCurrentUser("carol"); const carolDoc = await createEntityDoc("carol", "protected"); await share(carolDoc, "alice"); // a Link, to alice herself await post(docInbox, { payload: { onTheDocument: true }, ts: 2 }); // Alice connects: one call, both queues. setCurrentUser("alice"); await connectedUser(); expect(hasCap(carolDoc)).toBe(true); // the Link was applied expect(await readValues([protDoc], SECRET)).toEqual([]); // (protDoc holds no secret here) const left = await readInbox(docInbox); expect(left.map((d) => d.payload)).toEqual([{ onTheDocument: true }]); // consumer data stays }); // The same resolution property one level up: a user's own inbox. test("a third party resolves another user's inbox (the wallet level)", async () => { inject(); setCurrentUser("alice"); const aliceView = await userInbox("alice", "protected"); setCurrentUser("bob"); const bobView = await userInbox("alice", "protected"); expect(bobView).toBe(aliceView); });