refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph
Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever l'ambiguïté à chaque phrase — et le contrat publié, lu par une application, était le pire endroit pour laisser traîner ça. packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill, contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts → e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md. Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes, y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est fait occurrence par occurrence, pas par substitution. Le contrat énonce désormais son identité en une phrase : « This package is a polyfill of NextGraph's SDK. »
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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, 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 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);
|
||||
});
|
||||
Reference in New Issue
Block a user