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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user