/** * caps.test.ts — the cap surface as KEY POSSESSION. * * What these prove is a SHAPE, not a protection (the library is deliberately * insecure until cap-enforcement): the only question the registry can answer is "do I hold * this document's cap?", there is no principal to look up in a list, and no * function turns a bare reference into a cap. */ import { test, expect } from "bun:test"; import { CapRegistry, mintCap } from "../src/emulated-verifier/caps"; import { hasReadCap, targetOf } from "../src/model/nuri"; import type { ReadCap } from "../src/model/types"; /** A registry whose holder the test drives. */ function registry(initial: string | null = "alice") { let holder = initial; const caps = new CapRegistry(() => holder); return { caps, become: (id: string | null) => (holder = id) }; } test("a cap NAMES and READS; the bare reference only names", () => { const { caps } = registry(); const doc = "did:ng:o:doc1:v:overlay"; // Before anything: naming a document tells you nothing about reading it. expect(caps.capFor(doc)).toBeUndefined(); const cap = caps.mint(doc); expect(hasReadCap(cap)).toBe(true); // carries `:r:` expect(hasReadCap(doc)).toBe(false); expect(targetOf(cap)).toBe(doc); // same object, key inside expect(caps.capFor(doc)).toBe(cap); // Looking the cap up by the cap-bearing form resolves the same document. expect(caps.capFor(cap)).toBe(cap); }); test("no cap is derivable from a bare reference — you look it up or you were given it", () => { const { caps } = registry(); caps.mint("did:ng:o:mine"); // A document that never entered the held caps stays unreadable, however well-formed // its reference is. There is no `grantRead`, and no principal to name. expect(caps.capFor("did:ng:o:someone-else")).toBeUndefined(); }); // Passing the naming form where the reading form is meant is now a COMPILE error // (`ReadCap` is a template literal type). The runtime refusal still has to hold, // because a JavaScript consumer — or a cap read back from storage, a URL or JSON // and cast rather than narrowed — never meets the compiler. The `as` below is // exactly that consumer: it is how the mistake reaches the library at all. // Unchecked, it would file a bare reference as its own cap and make the document // read — the exact inversion this batch removes. test("learn REFUSES a bare reference, even when the compiler was bypassed", () => { const { caps } = registry(); const bare = "did:ng:o:someone-elses-doc" as ReadCap; // a JS consumer / an unchecked cast expect(() => caps.learn(bare)).toThrow(/naming is not reading|bare reference/i); expect(caps.capFor("did:ng:o:someone-elses-doc")).toBeUndefined(); // nothing was filed expect(caps.isEnforcing()).toBe(false); // and nothing was issued }); test("holding one document's cap grants nothing on another (no inheritance)", () => { const { caps } = registry(); caps.mint("did:ng:o:doc1"); expect(caps.capFor("did:ng:o:doc1")).toBeDefined(); expect(caps.capFor("did:ng:o:doc2")).toBeUndefined(); // separate repo, separate cap }); test("one set of held caps PER holder: switching identity switches heldByHolder, it does not wipe", () => { const { caps, become } = registry("alice"); const doc = "did:ng:o:alice-doc"; const cap = caps.mint(doc); become("bob"); expect(caps.capFor(doc)).toBeUndefined(); // bob holds nothing of alice's become("alice"); expect(caps.capFor(doc)).toBe(cap); // …and alice did not lose hers }); test("a cap received (learn) reads, exactly like one minted", () => { const alice = registry("alice"); const doc = "did:ng:o:shared"; const cap = alice.caps.mint(doc); const bob = registry("bob"); expect(bob.caps.capFor(doc)).toBeUndefined(); bob.caps.learn(cap); // delivered to bob's inbox, absorbed expect(bob.caps.capFor(doc)).toBe(cap); }); // A public store SERVES its documents' caps (`emulated-verifier/public-store.ts`). // This registry is one level below that: it records WHERE a document sits, and it // files a served cap apart from one that was minted or deposited — because the two // grant different things. test("markInPublicStore records where a document sits, and mints nothing", () => { const { caps, become } = registry("alice"); const doc = "did:ng:o:public-doc"; caps.markInPublicStore(doc); expect(caps.isInPublicStore(doc)).toBe(true); expect(caps.isInPublicStore("did:ng:o:other")).toBe(false); // Marking is not holding: the fact is about the document, the cap is about a holder. expect(caps.capFor(doc)).toBeUndefined(); become("bob"); expect(caps.capFor(doc)).toBeUndefined(); }); test("a cap SERVED by a public store is held like any other — possession is the read criterion", () => { const { caps, become } = registry("alice"); const doc = "did:ng:o:public-doc"; const served = mintCap(doc); become("bob"); caps.learnFromPublicStore(served); expect(caps.capFor(doc)).toBe(served); // No read-only mark, and that absence is the point. It existed until 2026-08-07 and // fed the write guard, which was the wrong predicate in both directions — writing is // OWNERSHIP, and how a read key arrived says nothing about it (see `reach.ts`). // Carol, in the same registry, holds nothing until she asks in her turn: what a public // store serves is per-asker, not once-for-everyone. become("carol"); expect(caps.capFor(doc)).toBeUndefined(); }); test("open(): a public document is marked as sitting in a public store, a private one is not", () => { const { caps } = registry(); const pub = caps.open("did:ng:o:pub", "public"); const prot = caps.open("did:ng:o:prot", "protected"); const priv = caps.open("did:ng:o:priv", "private"); expect(caps.isInPublicStore("did:ng:o:pub")).toBe(true); expect(caps.isInPublicStore("did:ng:o:prot")).toBe(false); expect(caps.isInPublicStore("did:ng:o:priv")).toBe(false); // All three are readable BY THEIR OWNER — a creator is never locked out. for (const [doc, cap] of [["did:ng:o:pub", pub], ["did:ng:o:prot", prot], ["did:ng:o:priv", priv]] as const) { expect(caps.capFor(doc)).toBe(cap); } }); test("open() is idempotent — re-listing my own documents refiles the same caps", () => { const { caps } = registry(); const first = caps.open("did:ng:o:doc", "protected"); let fired = 0; caps.onChange(() => (fired += 1)); expect(caps.open("did:ng:o:doc", "protected")).toBe(first); expect(fired).toBe(0); // nothing changed → no spurious re-read }); test("isEnforcing is false until the first cap exists, then holds for every holder", () => { const { caps, become } = registry("alice"); expect(caps.isEnforcing()).toBe(false); caps.mint("did:ng:o:doc1"); expect(caps.isEnforcing()).toBe(true); // …including for a holder whose own holds nothing: that IS the isolation. become("bob"); expect(caps.isEnforcing()).toBe(true); expect(caps.capFor("did:ng:o:doc1")).toBeUndefined(); }); test("a cap arriving fires the change signal — an asynchronous delivery must re-trigger reads", () => { const { caps } = registry(); let fired = 0; const unsub = caps.onChange(() => (fired += 1)); caps.learn(caps.mint("did:ng:o:doc1")); // mint fires once; the learn is a no-op expect(fired).toBe(1); unsub(); caps.mint("did:ng:o:doc2"); expect(fired).toBe(1); // unsubscribed }); test("write is restricted to write-cap holders (decorative until cap-enforcement)", () => { const { caps } = registry(); expect(caps.hasWritePolicy()).toBe(false); caps.grantWrite("did:ng:o:doc", "alice"); expect(caps.hasWritePolicy()).toBe(true); expect(caps.governsWrite("did:ng:o:doc")).toBe(true); expect(caps.governsWrite("did:ng:o:unknown")).toBe(false); // not declared → not enforced expect(caps.canWrite("did:ng:o:doc", "alice")).toBe(true); expect(caps.canWrite("did:ng:o:doc", "bob")).toBe(false); expect(caps.canWrite("did:ng:o:doc", null)).toBe(false); });