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:
@@ -416,6 +416,52 @@ test("connecting a user that does not exist provisions nothing", async () => {
|
||||
expect(getCaps().isEnforcing()).toBe(false);
|
||||
});
|
||||
|
||||
// WRITING IS OWNERSHIP — the two regressions that replaced the old write guard.
|
||||
//
|
||||
// It used to ask "was this cap served to me by a public store?", which was wrong in both
|
||||
// directions at once. Both are pinned here, because one predicate pushed two ways is
|
||||
// exactly how a fix trades one bug for a worse one.
|
||||
|
||||
// Direction 1 — TOO STRICT. The owner opening her own public note from its reference,
|
||||
// before her store has been listed (a deep link, a fresh session), got the "served by a
|
||||
// public store" mark on her own document and was refused a write to it.
|
||||
test("the owner writes to her own public note, even after opening it from its reference", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const pubDoc = await createEntityDoc("alice", "public");
|
||||
await write(pubDoc, SECRET, "v1");
|
||||
|
||||
// She arrives at it the way a deep link would: by reference, with nothing held.
|
||||
resetCaps();
|
||||
setCurrentUser("bob");
|
||||
await createEntityDoc("bob", "private"); // re-arms the emulation
|
||||
setCurrentUser("alice");
|
||||
await readValues([pubDoc], SECRET); // this is what files the served cap
|
||||
|
||||
await write(pubDoc, SECRET, "v2"); // must not throw
|
||||
expect((await readValues([pubDoc], SECRET)).includes("v2")).toBe(true);
|
||||
});
|
||||
|
||||
// Direction 2 — TOO LAX. A cap received in an inbox let its recipient WRITE into the
|
||||
// owner's document. Upstream impossible: writing is repo membership, and a Link is
|
||||
// "external repos only". An application could have shipped collaborative editing on it.
|
||||
test("a cap received in an inbox reads, and does NOT write", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const protDoc = await createEntityDoc("alice", "protected");
|
||||
await write(protDoc, SECRET, "alice's own");
|
||||
const BOB_INBOX = await userInbox("bob", "protected");
|
||||
await share(protDoc, "bob");
|
||||
|
||||
setCurrentUser("bob");
|
||||
await readInbox(BOB_INBOX);
|
||||
expect(await readValues([protDoc], SECRET)).toEqual(["alice's own"]); // he reads it
|
||||
await expect(write(protDoc, SECRET, "bob was here")).rejects.toThrow(/WRITE cap/i);
|
||||
|
||||
setCurrentUser("alice");
|
||||
expect(await readValues([protDoc], SECRET)).toEqual(["alice's own"]); // untouched
|
||||
});
|
||||
|
||||
// PER-DOCUMENT INBOXES. Upstream a repo carries `inbox: Option<PrivKey>` and its
|
||||
// owner records the private half with `AddInboxCap` on the User branch — the same
|
||||
// branch as `AddLink`. So "which inboxes may I read" has one answer, and connecting
|
||||
|
||||
Reference in New Issue
Block a user