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.
12 KiB
Brief — align on NextGraph's own model: users, stores, branches
REFUTED by adversarial review, 2026-07-30 — do not implement as written
Seven defects, four of them factual errors about NextGraph. The three that break the plan outright:
- D4 rests on a false premise. There IS a register for received caps:
AddLink { read_cap }on the User branch of the private store — "so that a user can share with all its device a new Link they received… Only external repos are accepted" (engine/repo/src/types.rs:1934-1950, verifier armcommits/mod.rs:681). It is wallet-resident and cross-device — the exact opposite of D4's per-browser localStorage. Corrected in../readcap-and-nuri-model.md§4quinquies.- D2's rejection of the named graph is factually wrong. A
GRAPH <…:v:…:b:…>quad IS resolved to that branch and committed on its own topic (engine/verifier/src/commits/transaction.rs:386-434); the verifier does exactly this inupdate_header. An unknown branch id errors rather than silently landing on Main. And a branch is a valid SPARQL-update target:TargetBranchV0::is_valid_for_sparql_updatereturns true forBranchId(engine/net/src/app_protocol.rs:77-82) — the fact table's claim to the contrary was wrong twice over.- The Store branch holds no triples at all.
BranchCrdt::None, "used by Overlay, Store and User BranchTypes" (engine/repo/src/types.rs:1420). It is a stream of service commits (AddRepo/RemoveRepo), not a graph — so all three D2 candidates were RDF inventions dressed as fidelity.And four more, all confirmed:
- D4 would delete a working recovery path. Inbox deposits are never removed (
packages/client/src/inbox.ts), so a second device/tab recovers its caps by re-reading. localStorage-without-re-reading loses them permanently, and contradicts P1a's delivered doctrine that per-process rebuild "is correct".- D3 is false outside entity documents.
capFor(scopeIndexDoc)andcapFor(walletInbox)are undefined before and afterlistMyEntityDocs— their caps can only ever be derived. Yet the boundary brief requires them reachable. Upstream that root comes from the wallet plusAddSignerCapon the User branch — a level the fact table omitted entirely.doc_createwrites four times, not two (+ the class quad on the Header branch, +AddSignerCapon the User branch).- Ordering defect: D2 before the boundary guard opens cap harvesting. Once caps are triples in
scopeIndexDoc(bob,…), and bothscopeIndexDocanddocs.sparqlQueryare exported,setCurrentUser("mallory")reads Bob's caps. TodaymintCapis unexported, so a NURI yields nothing. The guard must land before the caps become triples.Also flagged: "store" already means the native store in this codebase (
RegistrySession.privateStoreId), so D1'sprivateStore/storeDoccollide head-on; "the keyring notion disappears" contradictsreadcap-and-nuri-model.md§4quater, which calls the Store branch the owner's keyring; upstreamldp#containstakes an IRI object while the polyfill writes a literal, so D1 is not "nothing behavioural".Rewrite required. The verified facts are being folded back into
readcap-and-nuri-model.mdfirst; the plan is re-derived from there, not from this text.
Status: REFUTED 2026-07-30 — superseded, kept as the record of what was wrong. Original header follows.
Status: plan, not started. 2026-07-30. Companion to 2026-07-30-virtual-wallet-boundary.md (whose steps 3–4 are still pending) and to the caps chantier. This lot is about saying — and storing — what NextGraph says and stores, instead of a parallel vocabulary of our own.
Why this lot exists
Two PO corrections, 2026-07-30:
If NextGraph stores the key next to the document, then that is what we do. Without encryption we simply store a stand-in value. Stay as close as possible to how NextGraph works.
I don't like the name "scope container". Let's keep NextGraph's names. We adapt for the polyfill when we need to, but there too we must stay as close as possible.
And the vocabulary correction underneath both:
In the NextGraph code "wallet" is called "user", because a wallet is only a keyring. Virtual user → virtual user; physical user → physical user.
The library currently invents three things NextGraph does not have — a keyring, a scope index, a virtual user — and recomputes a key NextGraph stores. Each invention is a translation table a reader must carry, and each is a place where the consumer learns a model that will not exist.
Verified facts this plan rests on
All read in nextgraph-rs (git 213338f6) on 2026-07-30, recorded in full in ../readcap-and-nuri-model.md §4quinquies. The load-bearing ones:
| Fact | Source |
|---|---|
A wallet holds SEVERAL users: SensitiveWalletV0.sites: HashMap<String, SiteV0> |
engine/wallet/src/types.rs:434,457 |
A user (site) owns the three stores; UserId = PubKey |
engine/verifier/src/site.rs:23; engine/repo/src/types.rs:453 |
| The wallet holds ONE root key per user — the private store's read cap | site.rs:52 (get_individual_site_private_store_read_cap) |
doc_create writes TWICE: AddRepo { read_cap } on the Store branch, and ldp:contains on the Main branch |
engine/verifier/src/request_processor.rs:697-710; verifier.rs:2172-2199 |
| Replaying the Store branch is what reloads the repos with their keys | engine/verifier/src/commits/mod.rs:644-664 |
A branch is a NAMED GRAPH did:ng:o:{repo}:v:{overlay}:b:{branch}, with its own pub/sub topic and CRDT |
engine/net/src/app_protocol.rs:267-273; engine/repo/src/types.rs:1478-1501 |
| Anchoring on a document targets its Main branch | request_processor.rs:161-168 |
SPARQL cannot be anchored on a branch — every other target is unimplemented!() |
request_processor.rs:283 |
A Store branch exists only on a real store, created on a path doc_create does not take |
engine/repo/src/store.rs:425-440 |
A received cap has NO register: ContactDetails.read_cap is never read by the receiver |
engine/verifier/src/inbox_processor.rs:778-847 |
Durability of a received cap comes from OPENING the repo, which persists its read_cap in local user storage |
engine/verifier/src/user_storage/repo.rs:109,219,248,359; verifier.rs:542-544 |
Not verified, and therefore not relied on anywhere below: whether a GRAPH <…:b:…> write from the JS SDK round-trips through a real broker. It is assumed BROKEN and avoided.
Decisions
D1 — Vocabulary follows NextGraph
| Ours today | Becomes | Why |
|---|---|---|
| virtual user | virtual user | a wallet is a keyring; what owns three stores is a user (site) |
| physical user | physical user | NextGraph sees exactly one user; our identities are virtual ones |
keyring (CapRegistry) |
(the notion disappears — see D3) | there is no keyring object in NextGraph, and the wallet does NOT hold every key |
scope index / scope container (scopeIndexDoc, readScopeIndex, indexDocOf, INDEX_SUBJECT) |
store (storeDoc, readStore, …) |
the thing that lists a user's documents IS a store |
shim:contains |
ldp:contains |
NextGraph's own predicate for exactly this |
docPublic / docProtected / docPrivate on AccountRecord already read as stores; keep them, or rename to publicStore / protectedStore / privateStore for symmetry.
D2 — Emulate the Store branch as a distinct SUBJECT, not a distinct graph or document
The store document gains, beside its ldp:contains list, the read cap of each document it lists — the emulation of AddRepo { read_cap } on the Store branch.
Three candidate shapes were considered:
- A distinct named graph (
GRAPH <…:b:store>) — rejected. No branch would exist behind it; the content would be committed on the anchor's Main branch while claiming to live elsewhere. It misrepresents the structure, and its sync behaviour against a real broker is unverified (see above). - A separate document per branch — structurally closest (a document is what the JS SDK gives us that most resembles a branch: own topic, own sync, independently addressable), but it doubles the documents per store and adds an indirection to resolve them, to imitate a second pub/sub flow the polyfill will not use.
- A distinct subject in the same document — chosen. It buys what actually matters: the key stored next to the document, separate from the list, read rather than recomputed. It does not buy a second event flow, which the polyfill cannot reproduce anyway.
The honest cost of this choice: our two "branches" share one commit stream and one topic, where NextGraph has two. Anything that comes to depend on them being separately subscribable will have to move to the separate-document shape.
D3 — Deriving becomes minting, and CapRegistry stops being a keyring
Today fileOwnCaps re-derives each cap from its NURI, which only works because the emulated key is a function of the NURI. After D2 the cap is read from the store document. Derivation survives only inside nuri.ts as how a stand-in value is minted at creation — the single function P1b replaces with a real key.
The in-memory CapRegistry then stops being "the keyring" and becomes what it actually mirrors: the verifier's local user storage (fact table, last row) — the per-user cache of every opened repo and its read cap.
D4 — Received caps: persist as local user storage, not as a document
Verified: there is no received-caps register upstream, and inventing one would expose a shape the target does not have. What upstream does is persist the read_cap of every opened repo in local user storage.
So the emulation is a local, per-virtual-user store — the same nature as accounts.ts's existing IdentityStore (localStorage). This ends "re-read the inbox every session to recover caps", which the PO identified as the wrong model: an inbox is a queue you consume, not a store you re-read.
Open: whether to do D4 in this lot or after the boundary lot. It is the piece with the most design risk, and it is not needed for D1–D3 to be correct.
Plan
- D1 vocabulary, mechanically and in one pass — code, tests, docs. Nothing behavioural. Doing it first stops every later diff from being written in two vocabularies.
- D2 + D3: the store document carries each listed document's cap;
fileOwnCapsreads it instead of re-deriving;mintCapkeeps minting at creation only. Round-trip test: create → drop all in-memory state → re-list → the cap comes back read, not recomputed (assert by minting a different stand-in value in the test and checking the stored one wins). - D4 local per-user persistence of opened caps, replacing inbox re-reading.
- Then resume the boundary lot's steps 3–4 (guard at the four passage points; remove the cross-account fan-out), which are written in the new vocabulary.
What this breaks
storeRegistry's exported names change (scopeIndexDoc, listEntityDocs, AccountRecord fields). shim:contains becomes ldp:contains, so existing dev wallets stop resolving their documents — acceptable for dev data, and consistent with how the pointer/doc-shim migration was handled before, but it must be stated rather than discovered.
Risks I want challenged
- D2's "distinct subject" may be too weak a reading of "stay close to NextGraph" — the separate-document shape is defensible and I may be under-weighting it.
- D3 assumes reading the stored cap is always possible where deriving was — i.e. that every path reaching
fileOwnCapshas the store document at hand. - D4 introduces browser-local state to a library that currently keeps everything in the shared wallet; that may be a bigger departure than it looks.
- The vocabulary change touches the boundary brief and the P1a brief, which are mid-flight.