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`.
ng-eventually
A generic polyfill layer over the NextGraph JS SDK.
NextGraph's JS SDK does not yet expose cross-wallet reads, capabilities, inboxes
or group stores. ng-eventually lets a consumer application behave as if those
existed today, by emulating them on top of a single shared wallet / broker. It is
generic: it contains no application domain — the consumer application injects its
shapes and the acts of granting access.
The name: eventually NextGraph will ship these features; until then this layer fills the gap (and nods at eventual consistency / events).
The boundary — mature face out, compensation in
The asymmetry is the point. The consumer application writes SDK-shaped code as if NextGraph were finished: per-entity documents in public/protected/private stores, capabilities, inboxes. This library owns the current-state NextGraph knowledge and the simulation that fabricates that mature face — a shared-wallet emulation — so the application never sees it. As NextGraph matures, this library changes; the consumer application's code does not.
Docs (this library's own engineering doctrine, under docs/):
docs/nextgraph-current-state.md— the authoritative reference on what the current SDK/broker do and do not expose (the ground truth each polyfill compensates for).docs/simulation.md— how this lib emulates the mature behaviour on one shared wallet (shim, per-document ReadCaps, emulated inbox, write guard, the two axes, the double-proxy constraint).docs/read-model.md— the read model the polyfill implements: you follow links, you never enumerate; listing via a bounded set of per-doc anchoredsparql_querys; reactivity via re-query on a change signal.docs/decisions/— current-SDK ADRs (private-store scope, SPARQL delete, shared-wallet identity).docs/fork-inbox-fallback.md— the Rust-patch / self-host inbox path not taken (kept as a fallback).docs/migration-guide.md— the checklist for when real NextGraph matures.
What is emulated
Nothing in this library is a real NextGraph feature. Each behaviour below is emulated — a stopgap fabricated on top of the current, immature NextGraph (one shared wallet, everything physically readable). The consumer application always sees the mature SDK face; the emulation lives entirely here.
The table reads: what the consumer application does, the real NextGraph target it is written against, the current NextGraph implementation status (why a workaround is needed), and how this lib emulates it today.
| Capability | What the consumer application does | Real NextGraph target | Current NextGraph status (why a workaround) | Current emulation |
|---|---|---|---|---|
| Multi-identity / per-identity wallet | Treats each identity id as its own wallet with its own documents | Each identity opens its own real wallet; native cross-wallet reads | Not-yet-implemented: the JS SDK exposes no cross-wallet read, so one session cannot read another identity's wallet | One shared wallet everyone opens; "identities" are virtual users — shim accounts keyed by an id, each mapped to its documents in store-registry.ts |
| Three native stores per identity | Places entities by scope public / protected / private |
The identity's three real native stores hold the entity documents | Not-yet-implemented: doc_create/ORM can target only the private (and protected) native store today; a public/arbitrary StoreRepo is not JS-constructible |
Three emulated scope-index documents per account — each "store" is an index doc listing its entity-doc NURIs; all physically live in the one shared private store, and scope is a logical label |
| Per-document read isolation | Nothing to declare: creating a document records its cap on its store, and its creator holds it. Reading is capFor(doc) — you hold the key or you do not read |
The broker/verifier delivers only documents the wallet holds a ReadCap for; accessing a document without the cap yields an empty result in a union read (a targeted read of an unheld repo errors with RepoNotFound) |
The model itself is the point: reading is key possession, and there is no read-ACL to introspect — a client cannot ask "may this identity read this doc?" because that question does not exist upstream | Caps recorded per identity: AddRepo on the store's emulated Store branch for documents it creates, AddLink on its User branch for caps received; caps.ts caches them for the session. A read filter (read-filter.ts) plus the boundary (reach.ts) keep only documents whose cap is held. The cap value is the stand-in OK — enforcement is P1b |
| Directed read sharing | Owns the relationship concept ("who is connected to whom") itself, and on acceptance shares one document's cap to the other's inbox (shareCap(cap, theirInbox)) |
The cap sealed to the recipient's inbox key (ContactDetails.read_cap), opened by their own verifier while processing the inbox |
Not-yet-implemented — a gap, not a disagreement: the field exists but the message construction is unimplemented!(), its only caller passes "without read_cap", and the receiver discards the cap. The shape is right; the implementation is absent |
shareCap deposits the cap into the recipient's inbox document; the recipient's existing inbox.watch absorbs it into what they hold. No "receive" operation, and no principal is ever named to the registry |
| Inbox (registration notifications) | inbox.post / read / watch |
A message is sealed to the recipient's key and queued in their inbox; the recipient's own verifier unseals and applies each queued message inline while processing the inbox | Not-yet-implemented: the sender-side seal-into-inbox call (inbox_post_link) is proposed/future, not exposed in the JS SDK |
Deposits written as RDF into an inbox document via SPARQL; read/watch read the deposits back — an in-lib stand-in for the recipient's own inbox processing |
| Circulates the link itself — into inboxes, or into a document the reader already holds | There is no discovery. You cannot discover, you can only follow links: publishing = place the data in your public store and circulate the link, seen only by those who received it (a foundation of local-first) | Not a gap to be filled — a global index is not a NextGraph shape, and it would pool data across wallets | Nothing. discovery.ts and its global index were removed: they emulated a capability the target will never have. See docs/readcap-and-nuri-model.md §4ter-bis |
|
| Reads / listing | Lists the documents it needs, by scope, and reads them | Native per-wallet reads over the real per-identity stores | Bug/perf: an anchorless union query spans every named graph in the session store, which on a shared / accumulating wallet is O(wallet size) and stalls | A bounded, by-need set of per-doc anchored sparql_querys (each anchored to one repo's default graph), independent of wallet size |
| Reactivity | Lists update on change | Native reactive reads | Not-yet-implemented: there is no reactive union query across graphs | Re-query the bounded per-doc anchored set on a lightweight change signal (doc_subscribe / ORM on an already-opened single store) |
| Writes | Writes an entity to its scope | Writes land in the entity's real store via native primitives | Not-yet-implemented: doc_create can target only the private/protected store today (StoreRepo not JS-constructible) |
Per-entity documents via direct SPARQL (docs.sparqlUpdate on the real injected ng) |
| Current identity | Sets the current identity id (established at wallet import) via the SDK's current-identity call | Opening one's own wallet at the broker gate establishes the session identity | Not-yet-implemented for the shared-wallet case: everyone shares one wallet, so the broker cannot distinguish identities | A relayed id (accounts.ts IdentityStore persists it); the read filter and inbox from read it |
| Write-guard | Writes refused without the write cap | The broker/verifier enforces the write cap natively | Partial: the guard fires only on the public proxy, but the real write paths call the injected ng directly (the DataCloneError constraint), so it is best-effort today |
A sparql_update override (ng-proxy.ts) checking the emulated write cap |
Packages
| Package | Role |
|---|---|
@ng-eventually/client |
The SDK-identical wrapper the app imports instead of @ng-org/web / @ng-org/orm. It adds the polyfills the broker/verifier will do natively (shared-wallet identity, capability enforcement, anticipated cap/inbox methods). As NextGraph matures, the app points back at the real SDK (build alias removed) and this package falls away. |
A global-index package is deferred. In NextGraph an identity's apps and services see only what it shares, so there is no multi-identity backend. A global index would come from a singleton app (a global document administered by the developer), which is not implemented and undecided; simpler paths may exist. So there is no second package for now — it will be introduced once the global-index mechanism is decided, and it will be separate from the client.
Design principle
The application code is written as if the target NextGraph existed. All compensation lives here, beside the app. As NextGraph matures, this layer falls away; the app code (SDK-shaped) is unchanged.
- SDK-identical surface: the client wraps the real
ng(a Proxy that forwards everything and overrides only what must be emulated) anduseShape. The real SDK is injected viaconfigure()(no hard import → build-alias safe and testable). - Authorization is emulated capabilities: documents carry grants; the client enforces them generically (read filter + write guard). The app declares a document, shares one document's cap to an inbox — the same acts it will perform in the target. No policy is injected.
- Inbox: the client
inboxnamespace deposits (post) and, in the shared-wallet emulation, reads the deposits back (read/materialize/watch) in place of the recipient's own inbox processing. - Tests of the polyfill (against a real broker) live in this repo, so a consuming app can test its features against a clean, mocked API.
Status
Implemented. The polyfill mechanisms are wired against a real broker, not stubbed:
- Shared-wallet shim —
store-registry.ts((account, scope) → document NURI,createEntityDoc/listMyEntityDocs+ per-user stores, cross-device via the RDF shim anchored in the private store). - Document / SPARQL primitive —
docs.ts, calling the real injectedngdirectly (avoids the@ng-orgdouble-proxyDataCloneError). - Emulated ReadCaps —
caps.ts(CapRegistry, per-document, directed grants) + read filterread-filter.ts(reactive-setProxyview), applied byuse-shape.tsonly once a cap exists (caps.isEnforcing()). - Write guard —
ng-proxy.ts(sparql_updateoverride, emulated write cap). - Inbox —
inbox.ts(post/read/materialize/watch). - Identity —
accounts.ts(IdentityStore, injected storage). - SPARQL hardening —
sparql.ts(escapeLiteral/escapeIri/assertNuri).
The remaining TODO markers are narrow: the shared-wallet credential passthrough
in the session_start proxy branch, and the anticipated cap/inbox SDK signatures
to reconcile if the official API differs. See
docs/simulation.md for what each piece does and
docs/migration-guide.md for what changes as
NextGraph matures.