fix: écrire est une PROPRIÉTÉ, et trois portes qui n'auraient pas dû être ouvertes
Suite de la revue adverse. Quatre trous de frontière, tous hors du champ « l'isolation est fausse jusqu'à P1b » — P1b parle de matériau de clé, ceux-ci sont des défauts de FORME et resteraient des trous avec une vraie clé. **La garde d'écriture reposait sur la mauvaise question.** Elle demandait « ce cap m'a-t-il été servi par un store public ? ». Ce prédicat était faux dans les deux sens à la fois : trop laxiste — une clé reçue dans une inbox donnait l'écriture, alors qu'en amont un Link est « external repos only » et qu'écrire est l'appartenance au repo ; trop strict — la propriétaire de son propre document public était refusée dès qu'elle l'ouvrait depuis sa référence avant que son store ne soit listé. Un prédicat poussé dans deux sens est le signe que c'était le mauvais prédicat. Écrire dépend désormais de la PROPRIÉTÉ, lue sur la branche Store (l'`AddRepo` émulé), plus la paternité de session pour les documents créés par la primitive brute qui n'a aucun store où s'inscrire. Conséquence assumée et documentée : seul le propriétaire écrit, ce qui est l'état amont d'un repo tant qu'aucun membre n'a été ajouté — mécanisme qu'on n'émule pas. **`docs.depositInto` quittait la frontière en la publiant.** Sa doc disait « `inbox.post` est le seul appelant » : vrai dans la bibliothèque, faux dès qu'on le publie. Démontré : avec la seule référence nue d'un document public, on réécrit l'adresse d'inbox posée dessus et on détourne les dépôts destinés à son propriétaire. Une porte qui saute une garde ne doit pas être ouvrable par une application — elle rejoint la machinerie. **Le filtre de lecture n'interceptait que trois membres** et transmettait tout le reste lié à la CIBLE : `.values()`, `.map()`, `.getById()` rendaient le contenu d'un autre utilisateur — précisément les membres qu'une API de set réactif met en avant. Les membres qui rendent des éléments sont désormais filtrés, les mutations passent (elles ne rendent rien), et **tout membre inconnu lève** au lieu de transmettre : une transmission est une fuite silencieuse, une levée est bruyante et greppable. **Le mémo du store public était par document.** Le premier demandeur déclenchait le téléchargement, le cap était classé chez LUI, et tout demandeur suivant recevait « oui » en ne détenant rien. En amont un broker qui sert un overlay externe répond à TOUS. Le mémo garde la valeur, l'appelant la classe pour qui est connecté. Aussi : l'exemption `declareInfrastructure` supprimée — zéro appelant, ensemble toujours vide, et une doc décrivant deux documents exemptés qui ne l'ont jamais été. Et les caps d'écriture décrits comme « partiels » sont dits **inertes**, ce qu'ils sont : `grantWrite` n'a aucun appelant de production. **Ce que l'e2e a rattrapé.** Ma première version de la garde refusait au créateur l'écriture sur un document fait par `docs.docCreate` — 7 étapes rouges contre le broker, après une suite unitaire restée verte. La primitive brute n'inscrit la paternité nulle part ; c'est ce que `mintedHere` couvre désormais. 185 tests unitaires (dont quatre régressions : la propriétaire écrit, le destinataire non, le store public sert tout demandeur, aucun membre non filtré ne transmet), e2e 40/40 et applicatif 10/10.
This commit is contained in:
@@ -47,11 +47,17 @@
|
||||
*
|
||||
* ── 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 (`docs.sparqlQuery`,
|
||||
* the inbox, `store-registry`, `subscribe`, `open-repo`) are P1b. Nothing may be
|
||||
* claimed "anonymous" or "private" until then. The write caps below are likewise
|
||||
* decorative — the guard they feed (`ng-proxy`) is bypassed by every internal
|
||||
* writer; they are left as-is and belong to P1b.
|
||||
* 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.
|
||||
*
|
||||
* The write caps below (`grantWrite`, `governsWrite`, `canWrite`, `hasWritePolicy`) are
|
||||
* **inert, not partial** — a distinction the docs got wrong until 2026-08-07, when an
|
||||
* adversarial review measured it. `grantWrite` has NO production caller, so
|
||||
* `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.
|
||||
*/
|
||||
|
||||
import { CAP_SEGMENT, hasReadCap, targetOf } from "../model/nuri";
|
||||
@@ -86,6 +92,21 @@ const ANONYMOUS = "";
|
||||
export class CapRegistry {
|
||||
/** holder → the caps they hold, indexed by the cap-less NURI. */
|
||||
private heldByHolder = new Map<string, Map<Nuri, ReadCap>>();
|
||||
/**
|
||||
* holder → the documents they CREATED in this session, through {@link mint}.
|
||||
*
|
||||
* Authorship, for the one path that records it nowhere else. `storeRegistry`'s
|
||||
* documents are recorded durably on a Store branch (the emulated `AddRepo`, which is
|
||||
* what upstream's `doc_create` commits), so `ownsDocument` finds them on a later
|
||||
* session. The raw `docs.docCreate` has no store to record into — so nothing about
|
||||
* such a document survives its session, and an in-session note of who made it is
|
||||
* exactly as durable as the thing it describes.
|
||||
*
|
||||
* Consulted by the write guard before it pays for a Store-branch read. Without it the
|
||||
* guard refused a caller a write to a document it had just created — caught by the
|
||||
* live-broker e2e, seven steps red, after the unit suite stayed green.
|
||||
*/
|
||||
private mintedByHolder = new Map<string, Set<Nuri>>();
|
||||
/**
|
||||
* Documents this session knows to sit in a PUBLIC store — a fact about each
|
||||
* DOCUMENT, so global rather than per-holder, unlike everything else here.
|
||||
@@ -96,17 +117,6 @@ export class CapRegistry {
|
||||
* been, the holder holds it like any other and this set records only how it got there.
|
||||
*/
|
||||
private inPublicStore = new Set<Nuri>();
|
||||
/**
|
||||
* holder → the documents whose cap they hold ONLY because a public store served it
|
||||
* (see {@link learnFromPublicStore}).
|
||||
*
|
||||
* PER HOLDER, unlike the set above, and the difference is the whole point: *"this
|
||||
* document is in a public store"* is a fact about the document, whereas *"the only
|
||||
* claim I have on it is that the network handed me its key"* is a fact about one
|
||||
* holder. Kept global, the owner of a public document would be refused writes to it
|
||||
* the moment any third party fetched its cap.
|
||||
*/
|
||||
private servedByHolder = new Map<string, Set<Nuri>>();
|
||||
/** doc NURI → principals holding its WRITE cap. Decorative until P1b. */
|
||||
private writers = new Map<Nuri, Set<PrincipalId>>();
|
||||
/** Fired whenever a holder gains a cap — a cap delivered asynchronously must
|
||||
@@ -153,11 +163,6 @@ export class CapRegistry {
|
||||
);
|
||||
}
|
||||
const target = targetOf(cap);
|
||||
// Filing is the STRONG claim — I created this document, or its cap was deposited
|
||||
// for me. Either one supersedes "a public store served it to me", so the read-only
|
||||
// mark goes. {@link learnFromPublicStore} re-adds it after calling here, and only
|
||||
// when nothing was held before.
|
||||
this.servedToHolder().delete(target);
|
||||
const ring = this.heldCaps();
|
||||
if (ring.get(target) === cap) return false;
|
||||
ring.set(target, cap);
|
||||
@@ -173,9 +178,21 @@ export class CapRegistry {
|
||||
mint(nuri: Nuri): ReadCap {
|
||||
const cap = mintCap(nuri);
|
||||
this.file(cap);
|
||||
const key = this.holder() ?? ANONYMOUS;
|
||||
let made = this.mintedByHolder.get(key);
|
||||
if (!made) this.mintedByHolder.set(key, (made = new Set()));
|
||||
made.add(targetOf(nuri));
|
||||
return cap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the current holder CREATE this document in this session? Authorship, and
|
||||
* therefore the right to write — see {@link mintedByHolder}.
|
||||
*/
|
||||
mintedHere(nuri: Nuri): boolean {
|
||||
return this.mintedByHolder.get(this.holder() ?? ANONYMOUS)?.has(targetOf(nuri)) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* File a cap I was GIVEN — an inbox deposit of kind `cap`, or a repo link found
|
||||
* in world-readable content. This is the ONLY way a cap arrives from
|
||||
@@ -206,28 +223,9 @@ export class CapRegistry {
|
||||
* clears it. So a public document of my own is never read-only to me.
|
||||
*/
|
||||
learnFromPublicStore(cap: ReadCap): void {
|
||||
const target = targetOf(cap);
|
||||
const alreadyHeld = this.heldCaps().has(target);
|
||||
this.file(cap);
|
||||
// Only when this is the ONLY reason I hold it — filing never downgrades a claim.
|
||||
if (!alreadyHeld) this.servedToHolder().add(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the ONLY reason the current holder holds this document's cap that a public store
|
||||
* served it? Then it grants reading and nothing more — see {@link learnFromPublicStore}.
|
||||
*/
|
||||
isReadOnlyPublicCap(nuri: Nuri): boolean {
|
||||
return this.servedToHolder().has(targetOf(nuri));
|
||||
}
|
||||
|
||||
/** The current holder's public-store-served set, created on first use. */
|
||||
private servedToHolder(): Set<Nuri> {
|
||||
const key = this.holder() ?? ANONYMOUS;
|
||||
let s = this.servedByHolder.get(key);
|
||||
if (!s) this.servedByHolder.set(key, (s = new Set()));
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do I hold the cap of `nuri`? Returns it, or `undefined` when I hold
|
||||
@@ -353,7 +351,7 @@ export class CapRegistry {
|
||||
* NOT what an identity change does (that switches heldByHolder, see the header). */
|
||||
clear(): void {
|
||||
this.heldByHolder.clear();
|
||||
this.servedByHolder.clear();
|
||||
this.mintedByHolder.clear();
|
||||
this.inPublicStore.clear();
|
||||
this.writers.clear();
|
||||
this.issued = false;
|
||||
|
||||
Reference in New Issue
Block a user