Two batches, verified against nextgraph-rs throughout. P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>), the exact inversion of key possession. It is now possession: `capFor(nuri)` is the only question, there is no principal parameter anywhere, and nothing turns a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link deposit; receiving needs no operation. `Nuri` and `ReadCap` are template literal types, so passing a bare reference where a cap belongs is a compile error, with runtime guards behind it for JavaScript callers. The virtual user boundary. Every access function is now confined to the connected user, through two rules on one criterion (possession), implemented in two places so a lapse in either is caught by the other: authorization at the passage points, and "do not even attempt" at the callers. The polyfill's own machinery moved to physical.ts — unguarded, never exported — which replaced an exemption list: the machinery no longer gets waved through the guard, it calls something the guard never saw. Removed, as emulating capabilities the target does not have: - discovery.ts and its global index. There is no discovery in NextGraph; you follow links. It also pooled user data across wallets. - the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts, loadShim), which was cross-user enumeration by construction. - resolveInboxAnchor, a single inbox common to every user. Caps are now stored where NextGraph stores them, and read back rather than recomputed: AddRepo on the store's Store branch for documents a user creates, AddLink on its User branch for caps received. 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. Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO have a register (AddLink), contrary to what this repo's notes claimed; and "wallet" upstream means keyring — what owns three stores is a user, so the vocabulary follows. The cap value is the constant OK: the only question the emulation answers is whether a cap is held. P1b replaces that one constant with a real key. After this the shape is right and the isolation is still fake. Nothing here may be described as anonymous or private.
7.0 KiB
Migration guide — when real NextGraph matures
The whole point of this library: the consumer application already writes SDK-shaped code, so when NextGraph ships cross-wallet reads, capabilities and inboxes, only this lib changes. The consumer application's code does not change. This is the checklist.
Guiding invariant
Every emulated piece has a 1:1 image in the real infra. Migration = swap the
emulation for the real primitive, remove the scaffold. If a piece of the emulation
has no clear target image, that is a drift signal (see
simulation.md).
Checklist
1. Emulated ReadCaps → real capabilities
The shape is already the target's (P1a): a ReadCap is the document's key, a
each identity holds a set of caps, and there is no read-ACL anywhere. So
this step swaps the emulated key for the real one, not the model:
caps.ts's per-identity record becomes the verifier's own local user storage — it was always the cache, not the register. The two durable registers we emulate (readCapon the store's Store branch,linkon its User branch) become the realAddRepo/AddLinkcommits. Remove the emulation; the wallet and the branches already hold them.nuri.ts's stand-in cap value — the constantOK— becomes the realr:{base64url(serde_bare(ObjectRef))}. It is one function (mintCap), because every path now READS a stored cap instead of recomputing one.hasReadCap/targetOfstay meaningful: ther:discriminant is upstream grammar, not ours.shareCap(cap, toInbox)becomes the native sealed delivery (inbox_post_linkandContactDetails.read_cap), andinbox.read's inline absorption becomes the recipient's own verifier applying queued messages. The consumer's call does not change.publishRepoLinkbecomesRepoLinkV0.- The read filter (
read-filter.ts) and the possession gate inread-model.readUnionare then dead code — the broker only delivers documents whose cap the wallet holds. Remove them. - The write guard (
ng-proxy.tssparql_updateoverride) is a separate axis and is decorative today (every internal writer bypasses the proxy); it belongs to the P1b batch, not here.
The access unit is already the document (@graph), matching the native per-repo cap
model, so this is a key-material step, not a reshape.
2. Place documents in real native stores
Today docCreate(..., undefined) writes every document into the shared wallet's
private store, and the public|protected|private scope is a logical label
in the shim (see the two-axes section in simulation.md).
doc_createcannot target a non-private native store today — verified:StoreRepois not JS-constructible from the SDK, so there is no way to pass a public/protected store as the create destination (docCreate's trailingstorearg is leftundefined→ private store). The private store works only because it opens withoutRepoNotFound.- When the SDK lets you construct/target a native store, the migration adds a
getNativeStore(scope)-style resolver returning the real store to pass as thedocCreatedestination, so the logical scope label becomes a real store placement. (No such helper exists yet — it is blocked on the SDK gap above.) - At that point
store-registry.tsmaps(account, scope)to the user's real store NURI instead of a document in the shared wallet; the per-scope index document (the store-container emulation) is replaced by the store itself. The surface facing the consumer application (createEntityDoc,listMyEntityDocs, resolvers) is designed to survive that swap unchanged.
3. Drop the resolver / shim
The sharedWalletShim (account → 3 scope-document NURIs, held in a subscribable
doc-shim reached via a write-once pointer in the store-root — see
nextgraph-current-state.md § The pointer → doc-shim
indirection) has no target equivalent — the target has no central directory. Remove
it entirely: store-registry.ts, configureStoreRegistry, the pointer + doc-shim
resolution, and the pointerGuard dep. Cross-wallet reads replace the fan-out;
per-user wallets replace the shared one.
4. Real inbox → drop the in-lib read emulation
Replace the emulated inbox.ts deposit (docs.sparqlUpdate into a shared-wallet
document) with the native inbox_post_link (proposed/future). On the read side the
recipient's own verifier unseals each queued sealed message and applies it inline
when it processes its inbox — there is no separate curator to build; the in-lib read
emulation simply goes away (see the deferred global-index note in the top-level
README and decisions/discovery-model.md). The
single global index replaces the cross-account fan-out.
5. Retire the identity store → real per-user login
Remove accounts.ts (the IdentityStore that persists the identity id in
localStorage) and the app-level "Connexion" screen. The technical broker gate
becomes the real per-user login
(see decisions/shared-wallet-login-flow.md).
The flow shape ("broker redirect → app") does not change.
6. Drop the isolation scaffold
isolation.ts (application-visibility scaffold) disappears against a
different piece of infra than the caps: real per-account wallets, and the
relationship concept the consumer application owns. Distinct axis from ReadCaps —
remove independently.
7. Remove the build alias — the client becomes the real SDK
The consumer application imports @ng-org/web / @ng-org/orm resolved to this lib
via a build alias during the polyfill period. Removing the alias makes those imports
resolve to the real SDK — the ng/useShape/inbox surface is SDK-identical, so
no consumer code changes. The one non-SDK call — configure(...) /
@ng-eventually/client/polyfill — is deleted. The lib itself disappears.
The one break already taken: declareConnections
P1a broke the consumer once, deliberately and early, so that migration would not.
The old surface was an ACL held in memory, which forced the consumer to re-declare
every grant on every session (declareConnections). That call disappears: with
delivered caps the grant moves to the moment a connection is accepted — one
shareCap(capFor(doc), theirInbox) per document shared — and it persists, because
the delivery lives in the recipient's inbox rather than in a map that empties at
reload. There is no analogue of protectedDocsOf + the re-derivation loop.
This is a consumer re-architecture, not an API swap, and it is the price of being coded against a model that will exist. Nothing else about the migration below touches consumer code.
What does not change
The consumer application's code. Shapes, screens, the acts of sharing access, entity→scope mapping, the relationship graph — all injected, all untouched. Migration is entirely inside this library plus removing the alias + the bootstrap call. That asymmetry — a mature SDK face outward, all compensation inward — is the library's reason to exist.