Files
ng-eventually/packages/client/test/caps.test.ts
T
Sylvain Duchesne 88914f50ae refactor(layout): ranger les modules par destin à la migration
Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement
(`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne
disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que
le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est
partagé — trois destins sans rapport.

Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que
l'`exports` du paquet et le code du consommateur ne bougent pas :

- `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur,
  pas d'I/O. Survit comme connaissance.
- `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible
  documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé.
- `emulated-verifier/` — les doublures de ce que le verifier fait nativement :
  possession, dépôt des caps, frontière, non-livraison, traitement des inbox,
  registres de branche, ouverture de repo. **C'est le dossier où diverger du
  modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de,
  jamais est — cette bibliothèque ne réside dans aucune couche de la cible,
  elle les référence.
- `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les
  identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de
  rester invisible depuis `surface/`. S'évapore, remplacé par rien.

`store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la
main ce que la frontière de dossier fait structurellement — c'est la meilleure
preuve interne du bien-fondé de ce rangement.

Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les
scissions des modules à cheval suivent.

157 tests unitaires, typecheck src/test/e2e vert.
2026-08-04 12:46:44 +02:00

166 lines
6.8 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("publishRepoLink 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.publishRepoLink(doc);
expect(hasReadCap(link)).toBe(true);
expect(targetOf(link)).toBe(doc);
expect(caps.isPublished(doc)).toBe(true);
expect(caps.isPublished("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.isPublished("did:ng:o:pub")).toBe(true);
expect(caps.isPublished("did:ng:o:prot")).toBe(false);
expect(caps.isPublished("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);
});