Sylvain Duchesne 54f8389e9e refactor(api): l'app nomme une personne ou un document, jamais une adresse d'inbox
L'app d'exemple a servi de juge, et elle a immédiatement montré ce que
l'inventaire ne montrait pas : pour partager une note elle résolvait l'inbox du
destinataire, pour lire ses messages elle résolvait l'adresse de la sienne. Deux
gestes qu'aucune application n'aura à faire une fois la chose native — donc deux
gestes qu'elle ne doit pas apprendre.

- `shareCap(cap, toUser)` remplace `shareCap(cap, toInbox)`. Partager est un acte
  envers quelqu'un ; où est son inbox regarde la bibliothèque.
- `inbox.readForDocument(doc)` : le propriétaire lit ses messages en nommant la
  note, comme le déposant la nomme pour en laisser un.
- `storeRegistry.userInbox` et `documentInboxAddress` sortent de la surface
  publiée. Ils restent joignables en interne, où le shim en a besoin.

Sortent aussi de `/polyfill`, chacun parce qu'une app qui code contre apprend ce
qu'il faudra désapprendre :

- `getCaps` / `CapRegistry` — la salle des machines. La question du consommateur
  est `capFor(doc)` : est-ce que je le détiens ? Le registre n'a ni successeur ni
  forme inerte ; ce qui s'appuie dessus sera à réécrire, pas à laisser en place.
- `getCurrentUser` — une app sait qui elle a connecté ; le redemander à la
  bibliothèque est une commodité du wallet partagé.
- `virtualUsers` / `IdentityStore` — se souvenir d'une identité entre deux
  sessions est aussi le travail de l'app en amont. L'écran d'accès persiste ce
  dont IL a besoin ; rien d'autre n'a à être exposé.

Reste sur `/polyfill` ce qu'une app appelle vraiment : `configure` et
`setCurrentUser`. Le reste y est du test ou de l'injection interne.

170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la
bibliothèque, l'exemple et le harnais.
2026-08-05 18:55:30 +02:00

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 anchored sparql_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 shared-wallet/account-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; emulated-verifier/caps.ts caches them for the session. A read filter (emulated-verifier/read-filter.ts) plus the boundary (emulated-verifier/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 reachable from JS: the verifier has no InboxPost arm, and no inbox method exists in @ng-org/web. (inbox_post_link is OUR proposed name from docs/fork-inbox-fallback.md, not an announced NextGraph API — no such symbol exists in nextgraph-rs.) 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
Discovery of all public events REMOVED 2026-07-30 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 (shared-wallet/virtualUsers.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 (surface/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. Data common to all of an application's users comes from a singleton app: a document or store shared by all users and hardcoded in the app, write-owned by the developer and delegable — but never to all users, so user contributions reach it through an inbox (nothing in NextGraph is freely writable by everyone). That is the direction the NextGraph developer has named; it is not implemented, and several points are still open (what exactly is hardcoded, how delegation travels, who materializes the inbox). So there is no second package for now — it will be introduced once the mechanism exists, and it will be separate from the client. See docs/nextgraph-current-state.md § Apps & services.

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.

Both halves are binding, and the second is the one that gets traded away. The SURFACE must be as close as possible to the future SDK — that much is obvious, it is what the consumer codes against. But the IMPLEMENTATION must be as close as possible to what NextGraph actually plans, and there is no exception to that. Where upstream's behaviour is known, it is a specification, not a reference: when it is known, hold to it. What "known" means here is narrow — read in nextgraph-rs or stated by the NextGraph developer, never inferred from what an npm package happens to expose, and never inferred from an absent implementation ("the engine has no X" says nothing about whether the target will).

The pressure to deviate never announces itself as a deviation. It shows up as a cost, a latency, an ergonomic wrinkle — a real one. Two instances, both caught only by asking the question:

  • Every document has a native inbox was written into the docs from general reasoning. It is false, and it had already become an implementation.
  • A per-document inbox was made to point at the owner's inbox, to avoid a measured cost (9m37 → 21m30 on the consumer's suite). It emulates a many-to-one relation upstream cannot express: the verifier routes by inboxes: PubKey → RepoId and unseals with that one repo's key (engine/verifier/src/verifier.rs:1677,1928), and a message carries no target document because it needs none. Reverted. The cost was then solved without touching the shape — only documents meant to receive open an inbox.

The tell in both: an implementation choice that would make the consumer learn something it must unlearn at migration. That is the thing this library exists to prevent, so it outranks cost, latency and convenience. When the shape and the cost conflict, keep the shape and attack the cost elsewhere — and if it truly cannot be solved, say so rather than bend the model quietly.

The three references, numbered bottom-up

"NextGraph" is not one layer, and conflating them is how a fact about one gets asserted about another. They are stacked, each built on the one below, so they are numbered from the bottom:

# Layer Where
3 JS SDK / ORM @ng-org/orm, @ng-org/shex-orm — source in sdk/js/orm (TypeScript)
2 wasm binding @ng-org/web — source in sdk/js/lib-wasm (77 exported methods)
1 Rust engine engine/repo, verifier, net, broker, wallet

These are REFERENCES, not places we write code. Every line this library ships lives in the polyfill; none of these three layers is ours to touch, and nextgraph-rs is a read-only source of truth. Saying "level 1" about a piece of our code means "it is aligned on the engine's model" — never "it lives in the engine".

Which reference to align on: take the HIGHEST one that answers, and go down only when it does not.

  • Level 3 answers fully → do not implement it here. Pass through. Compensation code that doubles a working SDK function is code to delete later, and it diverges meanwhile.
  • Level 3 is absent or unsatisfactory → align on the level-2 call that does the job. Ergonomics are lost, semantics are kept — and migrating later means moving up one step, not rewriting.
  • Nothing at level 2 either → align on the level-1 MODEL: cardinalities, addressing units, what a structure can and cannot express.

Level 1's facts are the hardest-won, but aligning there means inventing a JS surface, since none exists yet. So always say which level a choice came from. A level-3 passthrough is a fact; a level-1 shape is a bet constrained by the engine. Presenting them alike is what manufactures false certainty — inbox_post_link was cited across eight files as a planned NextGraph API when it was only a name proposed in docs/fork-inbox-fallback.md.

Concretely for the inbox: level 3 has nothing, level 2 has no inbox method at all (and the verifier has no InboxPost arm), so inbox.* is aligned on level 1 — the engine's model (one inbox ↔ one repo, addressed by (overlay, pubkey), no target document in the message) with a JS surface of our own making.

This cascade answers "we need X — what do we align on?". It is not a checklist to run over what the target exposes: an unused binding method is not a debt, and "it was in the unused list" is not a reason to investigate it.

Do not confuse these levels with the other "three levels" in this repo. docs/readcap-and-nuri-model.md §4quinquies numbers where a cap is stored (wallet root key → the Store/User branch registers → the local in-memory cache). Same word, unrelated axis: these three are layers of NextGraph to align on, those three are places a key lives. When it is not obvious from the sentence, say "reference level" or "storage level".

  • SDK-identical surface: the client wraps the real ng (a Proxy that forwards everything and overrides only what must be emulated) and useShape. The real SDK is injected via configure() (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 inbox namespace 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 — shared-wallet/account-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 injected ng directly (avoids the @ng-org double-proxy DataCloneError).
  • Emulated ReadCaps — emulated-verifier/caps.ts (CapRegistry, per-document, directed grants) + read filter emulated-verifier/read-filter.ts (reactive-set Proxy view), applied by surface/use-shape.ts only once a cap exists (caps.isEnforcing()).
  • Write guard — surface/ng-proxy.ts (sparql_update override, emulated write cap).
  • Inbox — inbox.ts (post / read / materialize / watch).
  • Identity — shared-wallet/virtualUsers.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.

S
Description
No description provided
Readme 2.4 MiB
Languages
TypeScript 100%