feat(inbox): un utilisateur a DEUX inboxes, comme en amont

Tranché par la cascade plutôt qu'en attendant une réponse : le niveau 3 (ORM) ne
dit rien des inbox, le niveau 2 non plus — `@ng-org/web` n'expose aucune méthode
contenant « inbox » et la session n'en publie aucune. C'est donc le modèle du
moteur qui décide, et il dit DEUX : un site porte une inbox sur son repo de store
public et une autre sur son protégé (`engine/verifier/src/site.rs:127-152`), les
seuls `AddInboxCap` du moteur, `new_store_default` n'en posant une que
`if !private`. Elles sont adressées séparément jusque dans les enregistrements de
contact, qui choisissent leur prédicat selon le profil visé — `ng:site_inbox` pour
un profil public, `ng:protected_inbox` sinon
(`engine/verifier/src/inbox_processor.rs:787,823-824`).

`userInbox(id)` en exposait une : une cardinalité que cette bibliothèque avait
inventée, et que le nom `walletInbox` avait contribué à masquer. Elle prend
désormais le scope, et le store PRIVÉ n'en a pas — d'où `InboxScope` plutôt que
`Scope` : demander l'inbox privée n'est pas une recherche qui ne rend rien, c'est
une question sans référent dans le modèle, et le type l'interdit.

`myInboxes` énumère les deux, `isOwnInbox` reconnaît les deux. Le shim garde un
triple par (user, scope).

160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
This commit is contained in:
Sylvain Duchesne
2026-08-04 16:19:49 +02:00
parent b62bfe1e63
commit 3257afe8c0
16 changed files with 103 additions and 45 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
# Internal contract — what `@ng-eventually/client` keeps off its surface, and what NextGraph does or would do about each subject
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/client/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.``src/index.ts` and `./polyfill``src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, plus `isNuri`/`hasReadCap` from `nuri.ts` and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (7 functions: `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`). `polyfill.ts` re-exports `CapRegistry` from `emulated-verifier/caps.ts`, `shareCap` from `inbox.ts`, `connectedUser` from `emulated-verifier/connect.ts`, `* as accounts` from `shared-wallet/virtualUsers.ts`, and the types `VirtualUserStorage`, `VirtualUserRecord`, `RegistrySession`. Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
@@ -247,7 +249,7 @@ export function inspectOutbox(): void;
**F3 — incomplete citation in `subscribe.ts`.** `subscribe.ts:31` cites the ORM fan-out abort as "`initialize.rs:125-128`" with no path. The file is `engine/verifier/src/orm/graph/initialize.rs`; lines 125-128 are the graph loop calling `self.open_for_target(&nuri.target, true).await?` — verified, the `?` propagates `RepoNotFound` and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts:34` routes through `surface/placement.ts`; `polyfill.ts:238` carries `accounts`) those are internal or `/polyfill`. That file is being edited concurrently; noted here, deliberately not fixed by this document.
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts:34` routes through `surface/placement.ts`; `polyfill.ts:238` carries `virtualUsers`) those are internal or `/polyfill`. That file is being edited concurrently; noted here, deliberately not fixed by this document.
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`polyfill.ts:145`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.