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:
Sylvain Duchesne
2026-08-03 12:17:40 +02:00
parent 9d3e2d2bfe
commit 88f396a7ac
13 changed files with 198 additions and 67 deletions
+13 -4
View File
@@ -229,10 +229,19 @@ function capOfPayload(payload: unknown): ReadCap | null {
* Reaching several recipients means calling this once per inbox, which is what the
* real model does too: each delivery is sealed to one recipient.
*
* Upstream this path is a GAP, not a disagreement: the field exists
* (`ContactDetails.read_cap`) but its message construction is `unimplemented!()`
* and the receiver discards the cap. The shape is right; the implementation is
* absent, so we emulate it meanwhile.
* Upstream this path is a GAP, not a disagreement — verified at both ends:
* - the field exists, `ContactDetails.read_cap: Option<ReadCap>`
* (`engine/net/src/types.rs:4233`), but building a message that carries one is
* `read_cap: if with_readcap { unimplemented!() }` (`types.rs:3786`);
* - and the receiver ignores it: `InboxMsgContent::ContactDetails` writes only
* `ng:site`/`ng:protected` + `ng:*_inbox` into a fresh contact document
* (`engine/verifier/src/inbox_processor.rs:778-830`), never `details.read_cap`.
*
* Do NOT read `InboxMsgContent::Link` as the intended channel either: it is a **unit
* variant carrying nothing** (`engine/net/src/types.rs:4251`).
*
* The shape is right; the implementation is absent at both ends, so we emulate it
* meanwhile.
*/
export async function shareCap(cap: ReadCap, toInbox: Nuri): Promise<void> {
if (!hasReadCap(cap)) {