feat(client): OFF-by-default document access log, prefixed by active identity

Observability probe for the shared-wallet isolation footgun: on one physical
wallet several virtual identities coexist, and a read must never surface a doc
scoped to another identity. When it does (B reading A's doc), the leak is
invisible in the data — it looks like a normal read. This makes it VISIBLE.

Every real read/write is logged, prefixed by the ACTIVE virtual identity
(getCurrentUser → the account the op is scoped under, NOT the constant shared
physical wallet id). Reads append the row count — a strong leak signal:

  [urn:festipod:user:bob] READ did:ng:o:docA (readDoc) → 3 rows

Instrumented at the LOW common point in docs.ts: every read routes through
sparqlQuery, every write through sparqlUpdate, container creation through
docCreate. Callers pass a semantic label (readDoc|readUnion|listMyEntityDocs|
writeEntity|deposit|…) that is a lib-internal probe param, NOT forwarded to the
real `ng` (preserves docs.test.ts exact-forwarding assertions).

OFF by default → one boolean read on the hot path, zero output. On via
configure({ debugAccessLog: true }) or env NG_EVENTUALLY_ACCESS_LOG=1 (no code
change). Polyfill-era; removed at the real multi-store migration.

tsc --noEmit: 0 errors. bun test: 91 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-07 21:20:47 +02:00
parent dd1313258f
commit d8c36bac3b
6 changed files with 133 additions and 7 deletions
+9
View File
@@ -11,6 +11,7 @@
import type { NgLike, UseShapeLike, PrincipalId } from "./types";
import type { RegistrySession } from "./store-registry";
import { CapRegistry } from "./caps";
import { setAccessLog } from "./access-log";
/**
* Consumer-injected dependencies of the storeRegistry (polyfill-era). The
@@ -34,6 +35,13 @@ export interface EventuallyConfig {
sharedWallet?: { name: string; secret: string };
/** Initial current user; may also be set later via {@link setCurrentUser}. */
currentUser?: PrincipalId;
/**
* Turn on the OFF-by-default document access log (see {@link ./access-log}):
* every real read/write is printed, prefixed by the active identity, to
* diagnose the shared-wallet isolation leak. Also enablable without a code
* change via the env var `NG_EVENTUALLY_ACCESS_LOG=1`. Default: false.
*/
debugAccessLog?: boolean;
/** REAL `@ng-org/web` `init` (lifecycle) — forwarded by the lib's `init()`. */
init?: (...args: any[]) => any;
/** REAL `@ng-org/orm` `initNg` (ORM signals) — forwarded by the lib's `initNg()`. */
@@ -50,6 +58,7 @@ let caps = new CapRegistry();
export function configure(c: EventuallyConfig): void {
cfg = c;
currentUser = c.currentUser ?? null;
setAccessLog(c.debugAccessLog ?? false);
}
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */