fix: second tour adverse — mes correctifs avaient quatre trous, dont un qui les annulait
J'ai relancé un adversaire sur les correctifs du commit précédent, comme la règle l'exige. Il en a trouvé quatre. Le premier annulait la garde que je venais d'écrire. **Le registre de propriété était écrivable depuis la surface publiée.** `assertMayWrite` lit la propriété dans l'index de store de l'appelant — et `caps.open` marquait les documents de STRUCTURE (les trois stores, les inbox) comme « créés par moi ». Donc un porteur pouvait, par le `docs.sparqlUpdate` publié, insérer `contains "<n'importe quel document>"` dans son propre index et s'en déclarer propriétaire. Démontré : Bob écrit dans le document protégé d'Alice, et détourne l'inbox d'un de ses documents — exactement le vecteur que le commit précédent prétendait fermer. `open` classe désormais sans marquer : un document de structure n'est possédé par personne au sens de la paternité, donc les deux moitiés de la garde répondent non, ce qui est correct. **`inbox.post` acceptait n'importe quel NURI.** Déplacer `depositInto` hors de la surface ne suffisait pas : `post` atteint la même porte, qui saute les deux gardes par conception. Bob, ne détenant rien, écrivait quatre triplets dans le document d'Alice. En amont la confusion est impossible — `InboxPost` scelle vers une CLÉ d'inbox et le broker route par `inboxes: PubKey → RepoId` ; adresser un document n'est pas refusé, c'est inexprimable. Le shim tient maintenant un index des inbox, l'équivalent émulé de ce que le broker sait par construction, et `post` refuse ce qui n'en est pas une. **Le filtre de lecture fuyait encore par les clés dunder.** `DeepSignalSet` expose la collection brute sur `__raw__` / `__meta__` : `[...view]` rendait zéro élément pendant que `view.__raw__` rendait le Set complet, tous utilisateurs confondus. Mon en-tête affirmait qu'« une propriété simple ne porte aucun élément » — faux pour ce type. **Et il cassait des membres légitimes** : ma liste blanche couvrait la moitié des helpers d'itération, si bien que `toArray`, `reduce`, `first`, `take`, `drop`, `flatMap` levaient sur les données du porteur lui-même. Tous filtrés désormais ; le refus ne vaut que pour l'inconnu. **Deux tests réparés à la source plutôt qu'en affaiblissant les gardes.** Le faux `doc_create` de `inbox.test.ts` rendait une CONSTANTE — tous les documents créés étaient le même NURI, donc la garde de propriété n'avait rien à distinguer et deux tests lisaient l'inbox d'Alice sous l'identité de Bob sans que rien ne proteste. Et le harnais e2e utilisait un document ordinaire comme inbox. Enfin, mon propre cache d'inbox a reproduit la faute que la revue avait relevée ailleurs : un mémo qui survit à sa session. Rattaché à `resetRegistryCache`. 189 tests unitaires (six régressions de plus), e2e 40/40 et applicatif 10/10 — après un échec réseau non reproductible, relancé sans modification.
This commit is contained in:
@@ -73,7 +73,13 @@ function makeFakeNg() {
|
||||
for (const cb of subs.get(anchor) ?? []) cb({ V0: { Patch: { doc: anchor } } });
|
||||
};
|
||||
|
||||
const doc_create = mock(async (..._a: unknown[]) => "did:ng:o:new");
|
||||
// Distinct NURIs, one per creation — as a real broker does. It returned the CONSTANT
|
||||
// `"did:ng:o:new"` until 2026-08-07, so every document the library made was the same
|
||||
// one: two users' inboxes collided, and the ownership guard could not fire because
|
||||
// there was nothing to tell apart. An adversarial review measured it. A fake that
|
||||
// produces a state the real system never produces makes its suite green and blind.
|
||||
let created = 0;
|
||||
const doc_create = mock(async (..._a: unknown[]) => `did:ng:o:new${++created}`);
|
||||
|
||||
// Parses one deposit: `<subj> a <Deposit> ; <payload> "..." ; <ts> "..." [; <from> "..."] .`
|
||||
//
|
||||
@@ -202,9 +208,14 @@ test("(c) post rejects a spoofed `from` (naming another principal); self/null al
|
||||
expect(froms).toEqual(["alice", null]);
|
||||
});
|
||||
|
||||
test("from is optional — omitting it defaults to the current user", async () => {
|
||||
// Bob DEPOSITS, alice READS. The asymmetry is the model — anyone deposits, only the
|
||||
// owner reads — so a test that reads back under the depositor is testing a path no
|
||||
// application has. It passed until 2026-08-07 only because the fake `doc_create` handed
|
||||
// out one NURI for every document, so the ownership guard had nothing to tell apart.
|
||||
test("from is optional — omitting it defaults to the depositor", async () => {
|
||||
setCurrentUser("bob");
|
||||
await post(TARGET, { payload: { hi: 1 }, ts: 200 });
|
||||
setCurrentUser("alice");
|
||||
const deposits = await read(TARGET);
|
||||
expect(deposits[0]!.from).toBe("bob");
|
||||
});
|
||||
@@ -212,6 +223,7 @@ test("from is optional — omitting it defaults to the current user", async () =
|
||||
test("from: null makes an anonymous deposit even when a current user is set", async () => {
|
||||
setCurrentUser("bob");
|
||||
await post(TARGET, { from: null, payload: { hi: 1 }, ts: 200 });
|
||||
setCurrentUser("alice");
|
||||
const deposits = await read(TARGET);
|
||||
expect(deposits[0]!.from).toBeNull();
|
||||
});
|
||||
@@ -225,8 +237,13 @@ test("read returns deposits sorted by ts ascending and materialize is an alias",
|
||||
});
|
||||
|
||||
test("read is scoped to one inbox — deposits in another inbox are not returned", async () => {
|
||||
// The OTHER inbox is obtained from the system, not invented. A made-up NURI would be
|
||||
// a target no deposit can legitimately reach (`inbox.post` refuses what is not an
|
||||
// inbox), so the test would have been proving something the model does not allow.
|
||||
const otherInbox = await userInbox("bob", "protected");
|
||||
expect(otherInbox).not.toBe(TARGET);
|
||||
await post(TARGET, { from: null, payload: "mine", ts: 1 });
|
||||
await post("did:ng:o:other-inbox", { from: null, payload: "theirs", ts: 2 });
|
||||
await post(otherInbox, { from: null, payload: "theirs", ts: 2 });
|
||||
const deposits = await read(TARGET);
|
||||
expect(deposits.map((d) => d.payload)).toEqual(["mine"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user