0b37d17c2f
Les deux contrats et la doctrine portent des centaines de citations `file:line` vers `src/`. Le rangement par destin les périmait en bloc — 208 reprises (45 chemins `src/x.ts`, 163 mentions nues). Sans ça la réorganisation pourrissait l'instrument même qui tient la discipline qu'elle sert. Et un défaut relevé par le contrat interne, vérifié : `ng-proxy` fabriquait un membre `login`. `@ng-org/web` n'expose aucune méthode de ce nom — zéro occurrence dans les déclarations installées comme dans `sdk/js/lib-wasm/src/lib.rs` — mais le proxy répondait une fonction au lieu d'`undefined`, laquelle plantait à l'appel. C'était le seul endroit où ce wrapper ajoutait à la surface du SDK, contre son propre en-tête. 157 tests unitaires, typecheck src/test/e2e vert.
65 lines
8.3 KiB
Markdown
65 lines
8.3 KiB
Markdown
# Brief — a document's inbox has to be addressable by anyone holding the document
|
|
|
|
**Raised 2026-08-03, from the consumer side (Festipod), after an attempt to solve it in the app proved it does not belong there.**
|
|
|
|
> ## IMPLEMENTED 2026-08-03 — shape 2 (the library publishes), with the cost objection taken as binding
|
|
>
|
|
> `storeRegistry.documentInboxAddress(doc)` answers *"where do I deposit for this document"* for **any holder**, and `inbox.post` into it. The address is published **at creation**, in a compartment the library owns — so it never enters a consumer shape.
|
|
>
|
|
> **On the cost — and the wrong answer that was tried first.** The measured regression (9m37 → 21m30) came from creating a second **DOCUMENT** per document. The first fix pointed every document's published address at the owner's **own** inbox: no second document, cost amortized. **It was reverted the same day**, because it emulates a relation upstream cannot express — the verifier routes an incoming message by `inboxes: PubKey → RepoId` and unseals it with THAT repo's private half (`engine/verifier/src/verifier.rs:1677,1928`), and `InboxMsgBody` carries no target document (`engine/net/src/types.rs:4265`) because the address already identifies it. Many documents behind one inbox would have forced consumers to tag deposits with their document — a habit to unlearn at migration, which is precisely what this library exists to prevent.
|
|
>
|
|
> **The cost, actually answered:** only documents meant to RECEIVE open an inbox, and their owner is who knows. `createEntityDoc` publishes nothing; an app calls `openDocumentInbox(doc)` for the documents that need one (in the consumer's case: events, not every entity). Cost becomes proportional to the need, with the shape intact.
|
|
>
|
|
> Shape 1 of this brief (derivation) was **not available**: our inbox is a document, and a derived NURI would name a repo `doc_create` never created — upstream can derive because an inbox there is a keypair on the repo, not a document.
|
|
>
|
|
> **`inbox.postToDocument(doc, { payload })`** is the one call an app makes: it names the DOCUMENT, never an inbox, and **throws** when the document has no inbox rather than returning quietly — a deposit that vanishes without an error is the bug this whole path exists to close. There is deliberately **no target-document field on a deposit**, for the reason above.
|
|
>
|
|
> **Where the address lives.** On the document's emulated **Header branch** (`urn:ng-eventually:shim:headerBranch`), beside the content rather than in it — the same subject-as-compartment shape already used for the Store and User branches. `read-model` now drops every subject under the reserved `urn:ng-eventually:` namespace (`src/emulated-verifier/machinery.ts`), so the address cannot surface as one of the entity's properties. That filter is by SUBJECT, so it covers every emulated compartment present and future.
|
|
>
|
|
> **The open question, answered.** *"Is 'a document has an inbox addressable by any holder' upstream, or this library's invention?"* — **upstream, as a capability**; ours only as a JS surface. The engine lets any repo have an inbox: `inbox: Option<PrivKey>` is a field of every `Repo` (`engine/repo/src/repo.rs:126`), `AddInboxCapV0` is keyed by `repo_id` (`engine/repo/src/types.rs:1973`), and `update_inbox_cap_v0` applies it with **no `is_store` check** (`engine/verifier/src/verifier.rs:1920`), at any time (User-branch commit, `commit.rs:1043-1050`, whose type documents the update case). What no code path does is CREATE one for a document: `new_store_default` attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None` (`repo.rs:574`), and the engine's only two `AddInboxCap` commits are for the public and protected STORE repos (`site.rs:128,149`). "Does not" is not "cannot" — an earlier version of this note called the feature an anticipation on exactly that confusion. The half-split is upstream's too — a depositor seals with the inbox PUBLIC key (`engine/net/src/types.rs:4299`) and only the owner holds the private half — which is why an address is public by nature and belongs on the document, not on the owner's User branch.
|
|
>
|
|
> **Also fixed, and it was the root of the reported symptom.** `openDocumentInbox` (formerly `documentInbox`) called by a non-owner used to mint a parallel inbox and record it for the caller — no error, deposits lost. It now refuses, on OWNERSHIP (read from the Store branches), not on cap possession: a cap can be received, and a recipient must not be able to redirect the owner's deposits to itself.
|
|
|
|
## The problem, in one sentence
|
|
|
|
`documentInbox(doc)` answers *"which inbox do **I** know for this document?"* — and mints a fresh one when the answer is none. So a third party never reaches the owner's inbox: they get one of their own, which the owner never reads, and their deposit vanishes without an error.
|
|
|
|
## Why that breaks the consumer
|
|
|
|
Festipod's central act is signing up to **someone else's** event. The participant deposits into the event's inbox; the owner drains it at their next connection and materializes the count. With addressing scoped to the caller, only the owner can deposit into their own event — every other sign-up is silently lost.
|
|
|
|
The same wall stands in front of the directory document the app needs to rebuild discovery: creators must deposit a reference into a document they do not own.
|
|
|
|
## What was tried in the app, and why it was wrong
|
|
|
|
The consumer published the address on the document itself: resolve `documentInbox(eventDoc)` once at creation, write the NURI into the public event, and have participants read it from there instead of deriving it.
|
|
|
|
It works. It is still the wrong place, for three reasons — and the third is the one that settles it.
|
|
|
|
- **It puts infrastructure in the domain.** The `Event` shape starts carrying a technical address, and the domain model encodes a temporary state of the library.
|
|
- **It costs.** `documentInbox` *creates a document*. Publishing the address at creation takes every event creation from one document to two — and document creation is a round-trip that does not overlap. Measured on the test suite: the `@data` run went from **9m37 to 21m30**, with sign-up scenarios timing out on their setup step (4 failures out of 7 on a **fresh** profile, so not test-wallet bloat). That cost belongs to whoever can amortize or defer it. The app can do neither, because it does not own the mechanism.
|
|
- **The library already claims this job.** The commit that introduced per-document inboxes says: *"Inboxes belong to someone — the user's own, plus one per document — and connecting a user drains them all; that is the library's job, not the app's."* Draining was taken as the library's job. Addressing was left to the caller. Those two halves belong together.
|
|
|
|
The consumer's own doctrine says the same thing from the other side: when something does not work, the question is never how to work around it in the app, but what the library has to compensate. The app-side change has been reverted.
|
|
|
|
## What is being asked
|
|
|
|
**Given a document, any holder of that document should be able to name the inbox its owner reads** — without owning it, and without being handed the address out of band.
|
|
|
|
How is the library's call. Two shapes come to mind, neither prescriptive:
|
|
|
|
- Make the address **derivable** from the document, so `documentInbox(doc)` returns the same NURI for everyone, and only the owner can *read* it — the read guard already enforces that, and it is where the asymmetry belongs.
|
|
- Or have the library **publish** the address itself, at document creation, somewhere it controls — so it stays out of the consumer's shapes and the library keeps the freedom to make it lazy.
|
|
|
|
The second keeps the current create-time cost unless it is deferred; the first has none, and matches how the rest of the model works — an overlay is derived, a keyring is looked up, nothing is handed over out of band.
|
|
|
|
## Open question worth settling first
|
|
|
|
Is *"a document has an inbox addressable by any holder"* part of NextGraph's target model, or an invention of this library?
|
|
|
|
If it is upstream, this is emulation to align. If it is the library's own, it is a design decision to take deliberately — and the answer decides whether the note about it belongs in the shared NextGraph inbox as a gap, or here.
|
|
|
|
## Consumer state meanwhile
|
|
|
|
Sign-ups across identities do not converge, and the app does not pretend otherwise. Nothing in Festipod works around it.
|