refactor: les commentaires disent cap-surface et cap-enforcement

Suite du balayage commencé dans la doc : 20 occurrences de P1a/P1b dans les
commentaires, les titres de tests et le README.

Les phrases ont été récrites, pas substituées : « the breach P1a opened »
devient « the breach that cap-surface opened », et « labelled P1b's » ne
survivait pas à un nom plus long. Un lecteur qui n'a jamais entendu ni l'un ni
l'autre doit comprendre la phrase.

L'avertissement de déploiement du README garde sa force et gagne un nom :
« "anonymous" or "private" until cap-enforcement lands per-document
encryption. »

Reste une occurrence dans e2e/polyfill-entry.ts, qui part avec le lot e2e.
This commit is contained in:
Sylvain Duchesne
2026-08-11 19:14:49 +02:00
parent 2726f4a26f
commit 16e24f67f9
10 changed files with 35 additions and 26 deletions
+7
View File
@@ -0,0 +1,7 @@
# Doc-debt — app-contract
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED packages/polyfill/README.md @2026-08-11 (session f93872b5-293a-4916-a353-181409a96d42)
+1 -1
View File
@@ -22,7 +22,7 @@ Per-symbol, with the target signature and an epistemic label on every claim:
> has the shape of the real model — you hold a document's `ReadCap` or you do not read > has the shape of the real model — you hold a document's `ReadCap` or you do not read
> it, and there is no authorization list anywhere — but nothing is encrypted yet and > it, and there is no authorization list anywhere — but nothing is encrypted yet and
> the stand-in key is a constant. Nothing this library does may be described as > the stand-in key is a constant. Nothing this library does may be described as
> "anonymous" or "private" until per-document encryption lands (P1b). > "anonymous" or "private" until cap-enforcement lands per-document encryption.
```ts ```ts
import { import {
@@ -105,7 +105,7 @@ export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): v
const caps = getCaps(); const caps = getCaps();
// `learn(cap)`, not `open(doc, scope)` — the cap must be the SAME value that was // `learn(cap)`, not `open(doc, scope)` — the cap must be the SAME value that was
// written to the Store branch, not a second one minted from the NURI. They agree // written to the Store branch, not a second one minted from the NURI. They agree
// today only because the stand-in value is a constant; with a real key (P1b) a // today only because the stand-in value is a constant; with a real key (cap-enforcement) a
// second mint would produce a DIFFERENT key and the document would be unreadable // second mint would produce a DIFFERENT key and the document would be unreadable
// by the very session that created it. Mint once, store it, hold that one. // by the very session that created it. Mint once, store it, hold that one.
caps.learn(cap); caps.learn(cap);
+11 -10
View File
@@ -46,10 +46,11 @@
* reference. Filed apart (`learnFromPublicStore`) because it grants reading only. * reference. Filed apart (`learnFromPublicStore`) because it grants reading only.
* *
* ── What this module does NOT do ────────────────────────────────────────── * ── What this module does NOT do ──────────────────────────────────────────
* Enforce. The shape is right after P1a; the isolation is still fake. Per-document * Enforce. The shape is right, delivered by cap-surface; the isolation is still
* encryption and closing the read paths that bypass the guard (an ANCHORLESS * fake. Per-document encryption and closing the read paths that bypass the guard
* `docs.sparqlQuery`, the inbox, `store-registry`, `subscribe`, `open-repo`) are P1b. * (an ANCHORLESS `docs.sparqlQuery`, the inbox, `store-registry`, `subscribe`,
* Nothing may be claimed "anonymous" or "private" until then. * `open-repo`) are cap-enforcement's job. Nothing may be claimed "anonymous" or
* "private" until then.
* *
* The write caps below (`grantWrite`, `governsWrite`, `canWrite`, `hasWritePolicy`) are * The write caps below (`grantWrite`, `governsWrite`, `canWrite`, `hasWritePolicy`) are
* **inert, not partial** — a distinction the docs got wrong until 2026-08-07, when an * **inert, not partial** — a distinction the docs got wrong until 2026-08-07, when an
@@ -57,7 +58,7 @@
* `hasWritePolicy()` is permanently false and the `ng-proxy` guard they feed never fires * `hasWritePolicy()` is permanently false and the `ng-proxy` guard they feed never fires
* at all. Writing is governed instead by OWNERSHIP, at the write door (`reach.ts` * at all. Writing is governed instead by OWNERSHIP, at the write door (`reach.ts`
* `assertMayWrite`) — which is what upstream's `verify_permission` actually checks. These * `assertMayWrite`) — which is what upstream's `verify_permission` actually checks. These
* four are dead surface kept for P1b; do not read them as a working policy. * four are dead surface kept for cap-enforcement; do not read them as a working policy.
*/ */
import { CAP_SEGMENT, hasReadCap, targetOf } from "../model/nuri"; import { CAP_SEGMENT, hasReadCap, targetOf } from "../model/nuri";
@@ -74,7 +75,7 @@ import type { Nuri, PrincipalId, ReadCap, Scope } from "../model/types";
* own header, and put the emulation's one invented value in the file that claims to hold * own header, and put the emulation's one invented value in the file that claims to hold
* only verified target vocabulary. * only verified target vocabulary.
* *
* P1b replaces this single constant with a real key; migration deletes both. * cap-enforcement replaces this single constant with a real key; migration deletes both.
*/ */
const STAND_IN_CAP = "OK"; const STAND_IN_CAP = "OK";
@@ -117,7 +118,7 @@ export class CapRegistry {
* been, the holder holds it like any other and this set records only how it got there. * been, the holder holds it like any other and this set records only how it got there.
*/ */
private inPublicStore = new Set<Nuri>(); private inPublicStore = new Set<Nuri>();
/** doc NURI → principals holding its WRITE cap. Decorative until P1b. */ /** doc NURI → principals holding its WRITE cap. Decorative until cap-enforcement. */
private writers = new Map<Nuri, Set<PrincipalId>>(); private writers = new Map<Nuri, Set<PrincipalId>>();
/** Fired whenever a holder gains a cap — a cap delivered asynchronously must /** Fired whenever a holder gains a cap — a cap delivered asynchronously must
* re-trigger the reads that were empty for want of it. */ * re-trigger the reads that were empty for want of it. */
@@ -246,7 +247,7 @@ export class CapRegistry {
* It is a READ grant and nothing else. Upstream a public store makes its repos * It is a READ grant and nothing else. Upstream a public store makes its repos
* world-readable, never world-writable — writing needs the write cap, and * world-readable, never world-writable — writing needs the write cap, and
* `verify_permission` fires on WRITE only. Here the write guard still consults the * `verify_permission` fires on WRITE only. Here the write guard still consults the
* read cap (write caps are decorative until P1b, see the module header), so without * read cap (write caps are decorative until cap-enforcement, see the module header), so without
* this distinction a bare reference to a public document would buy a WRITE — a * this distinction a bare reference to a public document would buy a WRITE — a
* consumer would build on it, and have to unlearn it at migration. * consumer would build on it, and have to unlearn it at migration.
* *
@@ -308,7 +309,7 @@ export class CapRegistry {
* and when listing the holder's own documents back, which is how a holder's caps are * and when listing the holder's own documents back, which is how a holder's caps are
* rebuilt on a fresh session. * rebuilt on a fresh session.
* *
* Deliberately does NOT touch write caps: those are decorative until P1b, and * Deliberately does NOT touch write caps: those are decorative until cap-enforcement, and
* arming their guard here would be enforcement this batch does not do. * arming their guard here would be enforcement this batch does not do.
*/ */
open(nuri: Nuri, scope: Scope): ReadCap { open(nuri: Nuri, scope: Scope): ReadCap {
@@ -367,7 +368,7 @@ export class CapRegistry {
} }
} }
// --- write caps (decorative until P1b) ---------------------------------- // --- write caps (decorative until cap-enforcement) ----------------------------------
/** Grant `principal` the WRITE cap of document `doc`. */ /** Grant `principal` the WRITE cap of document `doc`. */
grantWrite(doc: Nuri, principal: PrincipalId): void { grantWrite(doc: Nuri, principal: PrincipalId): void {
@@ -106,8 +106,8 @@ export function assertMayReach(nuri: Nuri, op: string): void {
* *
* - too lax — a cap received in an inbox passed, so an application could write into a * - too lax — a cap received in an inbox passed, so an application could write into a
* document it merely reads. Someone could ship collaborative editing on it and lose * document it merely reads. Someone could ship collaborative editing on it and lose
* it at migration. It was labelled "P1b's", but P1b is key MATERIAL and this is a * it at migration. It was labelled cap-enforcement's responsibility, but
* model relation; * cap-enforcement is key MATERIAL and this is a model relation;
* - too strict — the owner of her own public document was refused, whenever she opened * - too strict — the owner of her own public document was refused, whenever she opened
* it from its reference before her store had been listed (a deep link, a fresh * it from its reference before her store had been listed (a deep link, a fresh
* session). The comment beside the code asserted the opposite. * session). The comment beside the code asserted the opposite.
+1 -1
View File
@@ -33,7 +33,7 @@
* can answer is **do I hold this document's cap, or not** — so the key value is the * can answer is **do I hold this document's cap, or not** — so the key value is the
* constant `OK`, which says exactly that and pretends nothing more. What identifies * constant `OK`, which says exactly that and pretends nothing more. What identifies
* the document is the NURI the key is attached to; the value carries no information. * the document is the NURI the key is attached to; the value carries no information.
* Real per-document encryption is P1b's job, and it replaces this one constant. * Real per-document encryption is cap-enforcement's job, and it replaces this one constant.
* Until then, possession is a SHAPE, not a protection. * Until then, possession is a SHAPE, not a protection.
*/ */
@@ -896,8 +896,8 @@ const inboxCache = new Map<string, Nuri>();
* recipient's inbox. That makes the inbox the **bootstrap of the whole * recipient's inbox. That makes the inbox the **bootstrap of the whole
* reachability graph** rather than a side feature — and it is why an inbox has to * reachability graph** rather than a side feature — and it is why an inbox has to
* BELONG to someone. Before this existed, an inbox was any NURI a caller passed, * BELONG to someone. Before this existed, an inbox was any NURI a caller passed,
* so "read the inbox" meant "read anyone's inbox", and since P1a routes caps * so "read the inbox" meant "read anyone's inbox", and since cap-surface routes
* through it, reading someone else's collected the caps addressed to them. * caps through it, reading someone else's collected the caps addressed to them.
* *
* Created on first sight and stable thereafter. Recorded in the doc-shim under its * Created on first sight and stable thereafter. Recorded in the doc-shim under its
* own predicate, read by its OWN query rather than added to the account SELECT: an * own predicate, read by its OWN query rather than added to the account SELECT: an
@@ -1144,7 +1144,7 @@ export async function listMyEntityDocs(id: string, scope: Scope): Promise<Nuri[]
// Recover the caps by READING the Store branch, never by recomputing them from // Recover the caps by READING the Store branch, never by recomputing them from
// the NURIs — that is the whole point of storing them. A fresh session gets back // the NURIs — that is the whole point of storing them. A fresh session gets back
// exactly what was recorded, and the day the stand-in value becomes a real key // exactly what was recorded, and the day the stand-in value becomes a real key
// (P1b) this path needs no change at all. // (cap-enforcement) this path needs no change at all.
// //
// Scoped to the current holder: another user's store caps are not ours to hold. // Scoped to the current holder: another user's store caps are not ours to hold.
const holder = getCurrentUser(); const holder = getCurrentUser();
+2 -2
View File
@@ -2,7 +2,7 @@
* caps.test.ts — the cap surface as KEY POSSESSION. * caps.test.ts — the cap surface as KEY POSSESSION.
* *
* What these prove is a SHAPE, not a protection (the library is deliberately * 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 * 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 * this document's cap?", there is no principal to look up in a list, and no
* function turns a bare reference into a cap. * function turns a bare reference into a cap.
*/ */
@@ -170,7 +170,7 @@ test("a cap arriving fires the change signal — an asynchronous delivery must r
expect(fired).toBe(1); // unsubscribed expect(fired).toBe(1); // unsubscribed
}); });
test("write is restricted to write-cap holders (decorative until P1b)", () => { test("write is restricted to write-cap holders (decorative until cap-enforcement)", () => {
const { caps } = registry(); const { caps } = registry();
expect(caps.hasWritePolicy()).toBe(false); expect(caps.hasWritePolicy()).toBe(false);
caps.grantWrite("did:ng:o:doc", "alice"); caps.grantWrite("did:ng:o:doc", "alice");
@@ -341,7 +341,7 @@ test("one held caps per virtual WALLET, not per spelling of its id", async () =>
expect(hasCap(doc)).toBe(false); expect(hasCap(doc)).toBe(false);
}); });
// THE BREACH P1a OPENED. Caps travel as inbox deposits, so an unguarded inbox read // THE BREACH THAT cap-surface OPENED. Caps travel as inbox deposits, so an unguarded inbox read
// let anyone who knew an inbox NURI collect the caps addressed to its owner — // let anyone who knew an inbox NURI collect the caps addressed to its owner —
// defeating directed sharing entirely. Depositing stays open (it is the only way a // defeating directed sharing entirely. Depositing stays open (it is the only way a
// link crosses between wallets at all); reading does not. // link crosses between wallets at all); reading does not.
@@ -393,7 +393,7 @@ test("a fresh session rebuilds the held caps from the scope index (the emulated
// The Store branch exists so a cap is READ back, not recomputed. Without this test // The Store branch exists so a cap is READ back, not recomputed. Without this test
// the two are indistinguishable: with a stand-in value, re-minting happens to give // the two are indistinguishable: with a stand-in value, re-minting happens to give
// the same string. So corrupt the stored cap and check the corruption wins — proof // the same string. So corrupt the stored cap and check the corruption wins — proof
// the value comes from the store, and proof that P1b's real key will too. // the value comes from the store, and proof that cap-enforcement's real key will too.
test("a document's cap is READ from the Store branch, never recomputed", async () => { test("a document's cap is READ from the Store branch, never recomputed", async () => {
const ng = inject(); const ng = inject();
setCurrentUser("alice"); setCurrentUser("alice");
@@ -426,7 +426,7 @@ test("the listing and the caps are two separate records", async () => {
expect(subjects.has("urn:ng-eventually:shim:storeBranch")).toBe(true); // Store branch: readCap expect(subjects.has("urn:ng-eventually:shim:storeBranch")).toBe(true); // Store branch: readCap
}); });
// P1b will make the stand-in value a real, non-derivable key. The moment it does, // cap-enforcement will make the stand-in value a real, non-derivable key. The moment it does,
// any path that mints a SECOND cap instead of using the stored one breaks: the // any path that mints a SECOND cap instead of using the stored one breaks: the
// creator would hold a key that does not open its own document. This pins that the // creator would hold a key that does not open its own document. This pins that the
// creation path mints exactly once. // creation path mints exactly once.
+4 -3
View File
@@ -7,9 +7,10 @@ import { resetCaps, resetConfig } from "../src/shared-wallet/bootstrap";
// This suite injects a fake `ng` via configure() and declares WRITE caps — // This suite injects a fake `ng` via configure() and declares WRITE caps —
// which stay an authorization list on purpose: only READING is key possession // which stay an authorization list on purpose: only READING is key possession
// (P1a). The write axis is decorative until P1b (every internal writer bypasses // (delivered by cap-surface). The write axis is decorative until
// this proxy). Reset after each test so the docs.test.ts "not configured" guard // cap-enforcement (every internal writer bypasses this proxy). Reset after each
// still holds and no cap leaks into another suite. // test so the docs.test.ts "not configured" guard still holds and no cap leaks
// into another suite.
afterEach(() => { afterEach(() => {
resetConfig(); resetConfig();
resetCaps(); resetCaps();