feat(client): read filter — capability-based view over the reactive set

makeReadFilteredView wraps a DeepSignalSet in a Proxy whose iteration/size/forEach
yield only items the current user may read (canRead on the item's emulated grant), while
add/delete and the underlying reactivity pass through. filterReadable is the pure core.
useShape applies it only when a grantOf resolver is configured (else passthrough, so
ungranted apps are unaffected). grantOf/setGrantOf added to the polyfill surface. Mirrors
the broker delivering only authorized docs; removed at migration. 4 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-06-29 10:28:40 +02:00
parent f4ded6d8f7
commit 672067d513
4 changed files with 147 additions and 10 deletions
+10 -10
View File
@@ -1,16 +1,16 @@
/**
* Wrapped `useShape`: same signature as `@ng-org/orm`. The returned reactive set
* is filtered to what the current user may read (emulated caps). At migration
* this filtering disappears — the broker only syncs authorized documents.
* Wrapped `useShape`: same signature as `@ng-org/orm`. When a grant resolver is
* configured, the returned set is a read-filtered VIEW (only items the current
* user may read); otherwise it passes the real set through unchanged. At
* migration the filtering disappears — the broker only delivers authorized docs.
*/
import { getConfig } from "./polyfill";
import { getConfig, getCurrentUser, getGrantOf } from "./polyfill";
import { makeReadFilteredView } from "./read-filter";
export function useShape(shapeType: unknown, scope: unknown): unknown {
const { useShape: real } = getConfig();
const set = real(shapeType, scope);
// TODO(polyfill): wrap `set` so iteration yields only entries whose emulated
// grant satisfies access.canRead(grant, getCurrentUser()), while preserving
// the reactivity of the underlying DeepSignalSet. This is the trickiest piece.
return set;
const set = getConfig().useShape(shapeType, scope) as object;
const grantOf = getGrantOf();
if (!grantOf) return set; // no policy configured → passthrough (filter inert)
return makeReadFilteredView(set, grantOf, getCurrentUser);
}