3c981ffadb
La règle que je venais d'écrire bannissait « publish » sans réserve. Or le moteur emploie `publisher` 126 fois — `as_publisher`, `publisher_advert` — pour un rôle pub/sub sur un topic. Une règle appliquée à la lettre aurait fait rejeter du vocabulaire amont, ce qui est exactement l'inverse du but. Le principe est donc posé au-dessus, et il prime sur tout : là où la cible a un mot, c'est le mot, point. Ce qui reste banni est NOTRE « publier un document », qui n'a aucun référent en amont et recouvre trois actes distincts — placer dans un store public, rendre trouvable, remettre une clé. Ça n'autorise jamais à renommer ce que l'amont appelle `publisher`. `publisher`, `topic` et `advert` rejoignent le vocabulaire déclaré du contrôle de noms, avec la raison en commentaire. Le renommage `publishRepoLink` → `recordInPublicStore` reste justifié : cette méthode n'a pas de pendant amont, elle enregistre un document en store public (et, artefact d'émulation, lui frappe une clé).
166 lines
6.9 KiB
TypeScript
166 lines
6.9 KiB
TypeScript
/**
|
|
* caps.test.ts — the cap surface as KEY POSSESSION.
|
|
*
|
|
* What these prove is a SHAPE, not a protection (the library is deliberately
|
|
* insecure until P1b): 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 } 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);
|
|
});
|
|
|
|
test("recordInPublicStore returns a cap-bearing link; reading it still means HOLDING it", () => {
|
|
const { caps, become } = registry("alice");
|
|
const doc = "did:ng:o:public-doc";
|
|
const link = caps.recordInPublicStore(doc);
|
|
|
|
expect(hasReadCap(link)).toBe(true);
|
|
expect(targetOf(link)).toBe(doc);
|
|
expect(caps.isInPublicStore(doc)).toBe(true);
|
|
expect(caps.isInPublicStore("did:ng:o:other")).toBe(false);
|
|
|
|
// Publication is not a world-wide read grant: whoever HAS the URL reads it.
|
|
become("bob");
|
|
expect(caps.capFor(doc)).toBeUndefined();
|
|
caps.learn(link); // bob received the link (e.g. from the discovery index)
|
|
expect(caps.capFor(doc)).toBe(link);
|
|
});
|
|
|
|
test("open(): a public document is published as a link, 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 P1b)", () => {
|
|
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);
|
|
});
|