fix(caps): créer un document en donne le cap, + corriger 9 faits NextGraph
Le trou trouvé par l'e2e contre le broker en ligne : `docs.docCreate` ne
déposait aucun cap pour le créateur, donc un consommateur pouvait créer un
document par la primitive publique puis se voir refuser sa lecture et son
écriture. En amont c'est impossible — `doc_create` commite
`AddRepo { read_cap }` sur la branche Store du store, et le créateur le détient
dès le premier instant. Délibérément non répliqué dans `physical.ts` : les
documents du shim n'appartiennent à aucun utilisateur virtuel, et
`store-registry` classe leurs caps là où il sait à qui ils sont.
e2e : 22 passés / 8 échoués → 39 / 0. Les autres échecs venaient du harnais,
qui agissait comme une seconde identité sans l'établir, ou lisait un document
quelconque comme une inbox. Un run e2e contre un wallet persistant exige une
identité FRAÎCHE par run : `walletInbox(id)` rend l'inbox stable pour son
propriétaire — c'est son intérêt — donc un id fixe accumule les dépôts des runs
précédents (vert au 2e run, rouge au 3e, à code inchangé).
Revue adverse de la documentation, 9 défauts, tous vérifiés à la source avant
correction :
- « chaque document a une inbox native » est FAUX. Seuls les repos de store
public et protected en ont une (`site.rs:128,149`) ; `new_store_default` n'en
pose que `if !private` et `doc_create` laisse `inbox: None`. Le store privé
n'en a pas non plus. Ce que le code fait est donc une ANTICIPATION — assumée
et notée comme telle dans `documentInbox`, le brief et l'ADR discovery. Ce qui
est vérifié, c'est la FORME : `AddInboxCapV0` est clé par `repo_id`.
- `InboxMsgContent::Link` est une variante unit sans charge utile : l'inbox ne
transporte aucun ReadCap. `shareCap` était juste et le reste ; ses citations
sont complétées aux deux bouts (émetteur `unimplemented!()`, récepteur qui
ignore `details.read_cap`).
- les 3 stores appartiennent au user (`SiteV0`), pas au wallet ;
- le TODO `OpenRepo` ne concerne pas la lecture cross-wallet — il est dans
`open_branch_`, après `RepoNotFound` ; charger par cap, c'est
`load_repo_from_read_cap` ;
- la liste des méthodes JS était un sous-ensemble présenté comme la surface
(77 exportées) ;
- `outbox-log.ts` n'enregistre rien : il inspecte l'outbox du SDK ;
- l'ADR private-store-nuri-scope citait `orm_start_graph` au présent, remplacé
par `ensureRepoOpen` ;
- l'incident write-loss plaçait `disconnections_sender.send` dans `broker.rs` ;
- la section « Apps & services » n'a aucune citation et rien ne lui correspond
dans le moteur : marquée à re-confirmer, pas à citer comme vérifiée.
Aussi : `fileOwnCaps` n'existe plus (`holdOwnCap` / `readStoreCaps` /
`fileOwnStructure`) — pointeur mort corrigé dans `caps.ts`.
This commit is contained in:
@@ -115,7 +115,7 @@ Applies to every exported surface, including ones added later: **if it is expose
|
||||
1. ~~**Remove `discovery.***~~ — **DONE 2026-07-30.** `src/discovery.ts` and `test/discovery.test.ts` deleted; `INDEX_ACCOUNT`, `watchShape`'s public-scope fold and its discovery-index container subscription, `nurisFromRef`, the `submitToIndex` guard, and the e2e discovery block all removed. P1a's acceptance test did not need re-basing: `test/cross-user-access.test.ts` already proves the same property (a bare reference reads nothing, the link reads the document) on the model's own terms — following a link — so `watch-shape.test.ts` (e), which proved it on the discovery fold, was dropped. Docs realigned: the ADR is marked superseded, `read-model.md` now describes ONE regime (follow, never enumerate), and the root README's capability row records the removal.
|
||||
2. ~~**"My inbox" + the inbox read guard**~~ — **DONE 2026-07-30.** `storeRegistry.walletInbox(id)` gives every virtual user its own inbox document, created on first sight and recorded in the doc-shim under its own predicate (`shim:docInbox`), read by its OWN query so an account record written before this existed still resolves — the fixed account SELECT did not grow a fourth required field. `isOwnInbox(nuri)` is the predicate; `inbox.read` / `readSynced` (hence `watch`, which reads through it) refuse an inbox that is not the connected wallet's, and refuse outright when no identity is set. **Depositing stays open** — `post` / `shareCap` are untouched, because that is the one legitimate cross-wallet act. The shared `resolveInboxAnchor` (a reserved account's document, an inbox COMMON to every wallet) was removed: it was unused by the library and violated *nothing common*. Locked by `test/isolation-active.test.ts` *an inbox may be DEPOSITED into by anyone, and READ only by its owner*, which walks the exact breach — Alice deposits, cannot read back; Mallory knowing the NURI absorbs nothing; anonymous is refused; Bob reads his own and only then does the cap land.
|
||||
|
||||
*Not done, and deliberately*: per-DOCUMENT inboxes. Upstream every document has one; here only the wallet does. **The PO has ruled they must come** (2026-07-30) — *"it can come in a second step, but it must come"* — so this is a commitment, not an option. The guard predicate (`isOwnInbox`) is where they plug in: it answers "is this inbox mine?", which extends to "…one of my documents' inboxes" without changing a single caller.
|
||||
*Not done, and deliberately*: per-DOCUMENT inboxes. ~~Upstream every document has one~~ — **false, corrected 2026-08-03**: no document has an inbox upstream, and neither does the private store (see step 7's correction). Here only the wallet does. **The PO has ruled they must come** (2026-07-30) — *"it can come in a second step, but it must come"* — so this is a commitment, not an option. The guard predicate (`isOwnInbox`) is where they plug in: it answers "is this inbox mine?", which extends to "…one of my documents' inboxes" without changing a single caller.
|
||||
|
||||
### Two defects this step surfaced — the first still open, the second closed by steps 5–6
|
||||
|
||||
@@ -190,6 +190,8 @@ So the store-root pointer, the doc-shim and the account records go through the m
|
||||
|
||||
The asymmetry holds at both levels, and a test walks it: **anyone deposits** into a document's inbox (that is how a third party reaches its owner at all), **only the owner reads** it.
|
||||
|
||||
**Correction 2026-08-03 — this step ANTICIPATES, it does not mirror.** The paragraph above overstated the upstream fact, and an adversarial doc review caught it. Verified: `new_store_default` attaches an inbox only `if !private` (`engine/verifier/src/verifier.rs:2994`), and `doc_create` goes through `new_repo_default`, which leaves `inbox: None` (`engine/repo/src/repo.rs:574`). The **only** two `AddInboxCap` commits in the engine are for the public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). So today **no document has an inbox, and neither does the private store**. What is genuinely verified is the *shape*: `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), so the record accommodates an inbox on any repo. The step stands as delivered — a per-document inbox is what the consumer needs and what the record's shape supports — but it must be read as a bet on where upstream goes, not as an emulation of what it already does.
|
||||
|
||||
## Relation to P1b
|
||||
|
||||
P1b (per-document encryption, closing the read paths that bypass the guard) largely **becomes** this lot, better framed. Encrypting each document while leaving the wallet boundary open would be securing the windows with the door open — and conversely, once every access is confined to the connected wallet, "the emulated key is derivable" stops being the load-bearing weakness. This lot should therefore precede P1b, or absorb it.
|
||||
|
||||
Reference in New Issue
Block a user