feat(client): activate ReadCap isolation via current identity + connections

Isolation was dormant (no current identity ever set). Now: setCurrentUser
records who is reading; declareConnections(neighborsOf) grants each protected
document's read cap to owner + connections. Reads discriminate through the
ReadCap filter: private→owner, protected→owner+connections, public→all. Generic
(the consumer injects identity + connections). Write-guard coverage limits
documented honestly in docs/simulation.md (real write paths bypass the JS proxy;
full enforcement awaits native caps). isolation-active.test.ts proves the
protected+connections path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-03 23:59:27 +02:00
parent e0d88b5076
commit 7db5eef33f
4 changed files with 120 additions and 0 deletions
+41
View File
@@ -179,6 +179,47 @@ In a mono-store layout (every item in one repo) this is all-or-nothing on that
document — exactly the native behaviour, and why fine-grained isolation requires
one document per entity (axis B).
### Making the ReadCap ACTIVE — current user + connection-driven grants
The filter only discriminates once the consumer (a) tells the SDK **who is
reading** and (b) declares the access policy on the documents. Both are plain SDK
calls; the consumer never touches the registry internals:
- **`setCurrentUser(id)` (`polyfill.ts`)** — the SDK's "current identity" call.
`useShape`'s filtered view reads it lazily, so the delivered subset always
reflects the identity in effect at read time. Until it is set, the filter has no
principal and (per `canRead(doc, null)`) only public documents pass — which is
why isolation stayed **dormant** while the consumer never made this call.
- **`getCaps().open(doc, scope, owner)`** — declares a document's policy when the
consumer creates it: `public` → world-readable; `protected`/`private` → owner
reads, owner holds the write cap. `open` now also **remembers** `(scope, owner)`
per document so a later connection-driven grant can find the protected ones.
- **`declareConnections(connections)` (`polyfill.ts`)** — the SDK-shaped
**protected sharing act**. The consumer hands its social graph (a `Connections`:
who-is-connected-to-whom) and the SDK issues, for every **protected** document,
that document's read cap to the owner's direct connections
(`CapRegistry.grantReadToConnections`). Public docs stay world-readable; private
docs stay owner-only. Re-callable whenever the graph changes; additive and
idempotent. The consumer passes only principals — no document NURI, no store id.
The result is the target's discrimination reproduced end-to-end: **private** →
owner; **protected** → owner + connections; **public** → all. Proven in
`test/isolation-active.test.ts` (an unconnected principal is denied a protected
document, granted it after `declareConnections`, and reads the public document
throughout).
### Write-guard coverage (honest scope)
The emulated write guard (`ng-proxy.ts`, `sparql_update` override) enforces the
per-document write cap **on the public `ng` proxy only**. In practice the
consumer's write paths (`docs.sparqlUpdate`, ORM `ngSet`) call the **real injected
`ng` directly** — never the public proxy — for the validated `DataCloneError`
reason above. So the guard is **best-effort**: it fires for any write routed
through the public proxy, but the consumer's real write paths bypass it and are
**not** guarded today. This is a deliberate, recorded limitation of the emulation
(the write guard becomes effective only when the broker/verifier enforces caps
natively at migration); the READ side is what makes isolation observably active.
### Emulated ReadCap ≠ application isolation — they COEXIST
`isolation.ts` is a **separate, deliberately non-merged** axis: