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:
@@ -35,7 +35,7 @@ This **prevents the shortcuts** the adversary pointed out (#4/#6: reading the pl
|
||||
|
||||
**This inventory IS the scope of P1b.** The only existing guard (`caps.canRead`) is moreover a **set-membership ACL** — the very inversion the vision forbids.
|
||||
|
||||
1. **Two distinct reference shapes**: cap-less (names/locates without reading — aligned with the NURI without `:k:`) vs cap-bearing (id + key/token). Absent today.
|
||||
1. **Two distinct reference shapes**: cap-less (names/locates without reading — aligned with the NURI without `:r:`) vs cap-bearing (id + key/token). Absent today.
|
||||
2. **Grant = delivering a cap-token to a recipient** (emulating the sealing: the recipient *receives* the token in their inbox; it is **possession** of the token that authorizes reading — not an ACL row checked per principal).
|
||||
3. **Enforcement by possession**: readers (`read-filter`, `use-shape`) only see what they **hold the token for**, not "what they are in the readers set for".
|
||||
4. **Resolving a cap-less** = naming / proving existence / counting, **without** exposing the content (support for anonymous presence).
|
||||
@@ -75,16 +75,16 @@ Erroneous content kept below as a record:
|
||||
## Open questions
|
||||
|
||||
- **SETTLED (PO directive, 2026-07-21)**: we **simulate the crypto** (per-doc encrypted data, cap = key). "Semantics only" (a token registry) is **discarded** — it turns back into an ACL and lets the plaintext be read. What remains to settle is the **level** of simulation (real lightweight encryption vs masked read-model projection), **before P1**.
|
||||
- NURI representation, cap-less vs cap-bearing, in the emulation (mirror `:k:`).
|
||||
- NURI representation, cap-less vs cap-bearing, in the emulation (mirror `:r:`).
|
||||
- ~~Should **keyless fetch** be allowed (resolving a cap-less into existence/count without the content)~~ — **SETTLED, and negatively (2026-07-27)**: not constructible. Addressing itself presupposes the cap, so there is nothing to expose. See the corrected Q1 verdict below. Kept struck through rather than deleted: the hypothesis is intuitive and will otherwise be re-formed.
|
||||
- API migration: `declareConnections`/`grantRead` → `seal(cap, recipient)` + `inbox → received caps`. Breaks consumers (the app-side `declareConnections` disappears).
|
||||
|
||||
## P0 — "keyless-resolve" spike (the blocker, BEFORE any P1)
|
||||
|
||||
**Load-bearing question**: can a holder of a **cap-less reference** (`did:ng:o:{id}:v:{overlay}`, without `:k:`), **without ever reading the content**:
|
||||
**Load-bearing question**: can a holder of a **cap-less reference** (`did:ng:o:{id}:v:{overlay}`, without `:r:`), **without ever reading the content**:
|
||||
- **Q1 — Existence / fetch**: prove/retrieve the presence of the (encrypted) blocks from the broker? Or does the broker require a ReadCap/membership in order to serve the blocks?
|
||||
- **Q2 — Deletion**: distinguish "exists" from "deleted"? *(The FRAGILE point: NextGraph is an append-only CRDT — a withdrawal = a **tombstone commit** that one would have to **read** in order to know about → potentially **the key is required**. And the **decrement on leave** depends on it.)*
|
||||
- **Q3 — Confidentiality**: does the key (`:k:`) remain **required** in order to decrypt (keyless never gives the content)?
|
||||
- **Q3 — Confidentiality**: does the key remain **required** in order to decrypt (keyless never gives the content)?
|
||||
|
||||
**Why this is the blocker**: the whole **anonymous counter** (counting/validating cap-less refs without reading) AND the **decrement on leave** depend on it. **If NO** → "anonymous counter via cap-less ref" is **not constructible in the target** → Festipod must **not** code that shape (guaranteed rewrite). **If YES** → P1 exposes `resolveCapLess(nuri) → {exists|deleted}` (never any content), and the emulation simulates it faithfully.
|
||||
|
||||
@@ -109,11 +109,11 @@ Erroneous content kept below as a record:
|
||||
- **Withdrawal has to be a message, not an observation.** On the consumer side: an explicit *nudge*. The polyfill has **nothing** to emulate for that — it just must not pretend otherwise.
|
||||
- **Settled by the Q1 correction**: the anonymous counter can**not** rest on an existence validation — that is not constructible. So it rests on something **declarative**, which is acceptable (outside the security scope) as long as the **exposed shape does not lie**: do not expose an existence primitive that the target will not offer.
|
||||
|
||||
## P1a — the surface
|
||||
## P1a — the surface — **DONE (2026-07-28)**
|
||||
|
||||
**Extracted into its own brief: [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md).**
|
||||
**Extracted into its own brief: [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md), which records what landed where.**
|
||||
|
||||
This batch is **specified and ready to implement**; it has its own note so that one can code from it without wading through the retracted material of this document.
|
||||
The ACL inversion — *the central defect this whole chantier exists to fix* — is gone: `caps.ts` is a keyring, sharing is a per-document delivery to an inbox, and a bare reference reads nothing. **P1b is now the blocker for any privacy claim**: the emulated key is derived (hence reproducible) and the bypass inventory below is untouched.
|
||||
|
||||
In two lines: a single new type (`ReadCap`), a keyring (`capFor`), a per-document share to an inbox (`shareCap`) — and nothing else. The branded types, `resolveCapLess`, `receivedCaps`, `refOf`, `parseNuri` and `PrincipalId` were **discarded** after a double adversarial review; the reasons are in that note.
|
||||
|
||||
@@ -121,8 +121,8 @@ This brief remains the **overall effort**: P0 verdicts, P1b scope, P2–P4 batch
|
||||
|
||||
## Phase sketch
|
||||
|
||||
- **P1a** — **the surface**: one new type (`ReadCap`), a keyring (`capFor`), per-document sharing to an inbox (`shareCap`). **The only batch that blocks Festipod.** Specified in its own note: [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md). *(An earlier draft listed `DocRef`/`DocCap` branded types, `resolveCapLess` and a durable `sealCapTo` here — all three were **dropped** after adversarial review; the note says why.)*
|
||||
- **P1b** — **the enforcement**: per-doc encryption (cap = key) and closing out the inventory of bypasses. Without it the shape is right but the isolation remains false — so nothing "anonymous" can be claimed.
|
||||
- ~~**P1a** — **the surface**~~ **DONE 2026-07-28**: one new type (`ReadCap`), a keyring (`capFor`), per-document sharing to an inbox (`shareCap`). It was the only batch blocking Festipod, and it no longer does. See [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md) for what landed where. *(An earlier draft listed `DocRef`/`DocCap` branded types, `resolveCapLess` and a durable `sealCapTo` here — all three were **dropped** after adversarial review; the note says why.)*
|
||||
- **P1b** — **the enforcement**: per-doc encryption (cap = key) and closing out the inventory of bypasses. Without it the shape is right but the isolation remains false — so nothing "anonymous" can be claimed. **Requalified 2026-07-30**: the bypass inventory below is really a **virtual user boundary** problem, and it is now specified on its own in [`2026-07-30-virtual-wallet-boundary.md`](2026-07-30-virtual-wallet-boundary.md). That lot precedes or absorbs this one — encrypting each document while any wallet can reach any document secures the windows with the door open.
|
||||
- **P2** — replace the ACL with a **token possession** model (grant = deliver to a recipient; enforcement = possession). *Requalified by the adversarial review: the real content of P2 is **durability + cap-less + re-sharing by the holder**, not "inverting the ACL" — without crypto, inverting produces no observable delta.*
|
||||
- **P3** — revocation by re-key (invalidation + re-delivery, non-retroactive).
|
||||
- ~~**PW** — WriteCap = membership~~ **DROPPED (2026-07-27)**. This batch rested on a notion of membership that **does not exist** in the model (everything is keys and URLs); see the struck-through section above. It was moreover justified by a need for **dedup via signature verification** that the consumer turns out not to have: its dedup rests on the overlay, which is store-scoped. *For the record, two facts verified along the way, not to be re-discovered*: author signature verification **is not called at runtime**; and the author digest is **not** keyed under the read secret — it is keyed by the **outer** overlay, which is public *(it is the commit's **content** that is encrypted, hence the fact that verifying still presupposes being able to read)*. Detail in `nextgraph-current-state.md`.
|
||||
|
||||
Reference in New Issue
Block a user