Align the cap emulation on NextGraph's model, and confine it to a virtual user
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.
This commit is contained in:
+43
-9
@@ -14,13 +14,33 @@ has no clear target image, that is a drift signal (see
|
||||
## Checklist
|
||||
|
||||
### 1. Emulated ReadCaps → real capabilities
|
||||
Translate the per-document `CapRegistry` (`caps.ts`) into real NextGraph caps: the
|
||||
broker/verifier enforces them, and `useShape` already returns only authorized
|
||||
documents. The directed `grantRead(doc, granteeId)` maps to a native per-document
|
||||
ReadCap issued to that identity. The read filter (`read-filter.ts`) and the write
|
||||
guard (`ng-proxy.ts` `sparql_update` override) are then dead code — remove them. The
|
||||
access unit is already the document (`@graph`), matching the native per-repo cap
|
||||
model, so this is a data step, not a reshape.
|
||||
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
|
||||
@@ -39,7 +59,7 @@ in the shim (see the two-axes section in [`simulation.md`](./simulation.md)).
|
||||
- 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`, `listEntityDocs`,
|
||||
surface facing the consumer application (`createEntityDoc`, `listMyEntityDocs`,
|
||||
resolvers) is designed to survive that swap unchanged.
|
||||
|
||||
### 3. Drop the resolver / shim
|
||||
@@ -80,9 +100,23 @@ resolve to the real SDK — the `ng`/`useShape`/`inbox` surface is SDK-identical
|
||||
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 granting
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user