ae9c32e271
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.
124 lines
7.0 KiB
Markdown
124 lines
7.0 KiB
Markdown
# 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`](./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
|
|
(`readCap` on the store's Store branch, `link` on its User branch) become the real
|
|
`AddRepo` / `AddLink` commits. Remove the emulation; the wallet and the branches
|
|
already hold them.
|
|
- `nuri.ts`'s stand-in cap value — the constant `OK` — becomes the real
|
|
`r:{base64url(serde_bare(ObjectRef))}`. It is **one function** (`mintCap`), because
|
|
every path now READS a stored cap instead of recomputing one. `hasReadCap` /
|
|
`targetOf` stay meaningful: the `r:` discriminant is upstream grammar, not ours.
|
|
- `shareCap(cap, toInbox)` becomes the native sealed delivery (`inbox_post_link`
|
|
and `ContactDetails.read_cap`), and `inbox.read`'s inline absorption becomes the
|
|
recipient's own verifier applying queued messages. **The consumer's call does not
|
|
change.**
|
|
- `publishRepoLink` becomes `RepoLinkV0`.
|
|
- The read filter (`read-filter.ts`) and the possession gate in
|
|
`read-model.readUnion` are then dead code — the broker only delivers documents
|
|
whose cap the wallet holds. Remove them.
|
|
- The write guard (`ng-proxy.ts` `sparql_update` override) 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`](./simulation.md)).
|
|
|
|
- `doc_create` cannot target a non-private native store today — verified:
|
|
`StoreRepo` is not JS-constructible from the SDK, so there is no way to pass
|
|
a public/protected store as the create destination (`docCreate`'s trailing
|
|
`store` arg is left `undefined` → private store). The private store works only
|
|
because it opens without `RepoNotFound`.
|
|
- 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 the
|
|
`docCreate` destination, 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.ts` maps `(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`](./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`](./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`](./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.
|