diff --git a/.project/concepts/app-contract/_debt.md b/.project/concepts/app-contract/_debt.md new file mode 100644 index 0000000..99eebf1 --- /dev/null +++ b/.project/concepts/app-contract/_debt.md @@ -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) diff --git a/packages/polyfill/README.md b/packages/polyfill/README.md index 66bc568..b767879 100644 --- a/packages/polyfill/README.md +++ b/packages/polyfill/README.md @@ -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 > 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 -> "anonymous" or "private" until per-document encryption lands (P1b). +> "anonymous" or "private" until cap-enforcement lands per-document encryption. ```ts import { diff --git a/packages/polyfill/src/emulated-verifier/branch-registers.ts b/packages/polyfill/src/emulated-verifier/branch-registers.ts index a1cc428..4791c58 100644 --- a/packages/polyfill/src/emulated-verifier/branch-registers.ts +++ b/packages/polyfill/src/emulated-verifier/branch-registers.ts @@ -105,7 +105,7 @@ export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): v const caps = getCaps(); // `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 - // 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 // by the very session that created it. Mint once, store it, hold that one. caps.learn(cap); diff --git a/packages/polyfill/src/emulated-verifier/caps.ts b/packages/polyfill/src/emulated-verifier/caps.ts index d7bf6c8..5917c18 100644 --- a/packages/polyfill/src/emulated-verifier/caps.ts +++ b/packages/polyfill/src/emulated-verifier/caps.ts @@ -46,10 +46,11 @@ * reference. Filed apart (`learnFromPublicStore`) because it grants reading only. * * ── What this module does NOT do ────────────────────────────────────────── - * Enforce. The shape is right after P1a; the isolation is still fake. Per-document - * encryption and closing the read paths that bypass the guard (an ANCHORLESS - * `docs.sparqlQuery`, the inbox, `store-registry`, `subscribe`, `open-repo`) are P1b. - * Nothing may be claimed "anonymous" or "private" until then. + * Enforce. The shape is right, delivered by cap-surface; the isolation is still + * fake. Per-document encryption and closing the read paths that bypass the guard + * (an ANCHORLESS `docs.sparqlQuery`, the inbox, `store-registry`, `subscribe`, + * `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 * **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 * 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 - * 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"; @@ -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 * 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"; @@ -117,7 +118,7 @@ export class CapRegistry { * been, the holder holds it like any other and this set records only how it got there. */ private inPublicStore = new Set(); - /** 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>(); /** Fired whenever a holder gains a cap — a cap delivered asynchronously must * 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 * world-readable, never world-writable — writing needs the write cap, and * `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 * 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 * 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. */ 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`. */ grantWrite(doc: Nuri, principal: PrincipalId): void { diff --git a/packages/polyfill/src/emulated-verifier/reach.ts b/packages/polyfill/src/emulated-verifier/reach.ts index b58fb61..9276a0d 100644 --- a/packages/polyfill/src/emulated-verifier/reach.ts +++ b/packages/polyfill/src/emulated-verifier/reach.ts @@ -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 * 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 - * model relation; + * it at migration. It was labelled cap-enforcement's responsibility, but + * 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 * it from its reference before her store had been listed (a deep link, a fresh * session). The comment beside the code asserted the opposite. diff --git a/packages/polyfill/src/model/nuri.ts b/packages/polyfill/src/model/nuri.ts index 47030cc..67f6194 100644 --- a/packages/polyfill/src/model/nuri.ts +++ b/packages/polyfill/src/model/nuri.ts @@ -33,7 +33,7 @@ * 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 * 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. */ diff --git a/packages/polyfill/src/shared-wallet/account-registry.ts b/packages/polyfill/src/shared-wallet/account-registry.ts index 666a80f..f1ea3e5 100644 --- a/packages/polyfill/src/shared-wallet/account-registry.ts +++ b/packages/polyfill/src/shared-wallet/account-registry.ts @@ -896,8 +896,8 @@ const inboxCache = new Map(); * 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 * 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 - * through it, reading someone else's collected the caps addressed to them. + * so "read the inbox" meant "read anyone's inbox", and since cap-surface routes + * 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 * 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 { +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"); diff --git a/packages/polyfill/test/isolation-active.test.ts b/packages/polyfill/test/isolation-active.test.ts index d5a2e16..fbd4dc6 100644 --- a/packages/polyfill/test/isolation-active.test.ts +++ b/packages/polyfill/test/isolation-active.test.ts @@ -341,7 +341,7 @@ test("one held caps per virtual WALLET, not per spelling of its id", async () => 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 — // defeating directed sharing entirely. Depositing stays open (it is the only way a // 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 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 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 () => { const ng = inject(); 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 }); -// 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 // creator would hold a key that does not open its own document. This pins that the // creation path mints exactly once. diff --git a/packages/polyfill/test/ng-proxy.test.ts b/packages/polyfill/test/ng-proxy.test.ts index 80eada7..eecd3b6 100644 --- a/packages/polyfill/test/ng-proxy.test.ts +++ b/packages/polyfill/test/ng-proxy.test.ts @@ -7,9 +7,10 @@ import { resetCaps, resetConfig } from "../src/shared-wallet/bootstrap"; // This suite injects a fake `ng` via configure() and declares WRITE caps — // which stay an authorization list on purpose: only READING is key possession -// (P1a). The write axis is decorative until P1b (every internal writer bypasses -// this proxy). Reset after each test so the docs.test.ts "not configured" guard -// still holds and no cap leaks into another suite. +// (delivered by cap-surface). The write axis is decorative until +// cap-enforcement (every internal writer bypasses this proxy). Reset after each +// test so the docs.test.ts "not configured" guard still holds and no cap leaks +// into another suite. afterEach(() => { resetConfig(); resetCaps();