diff --git a/packages/client/src/access-log.ts b/packages/client/src/access-log.ts new file mode 100644 index 0000000..8da8b5b --- /dev/null +++ b/packages/client/src/access-log.ts @@ -0,0 +1,81 @@ +/** + * access-log — an OFF-by-default observability probe for document access. + * + * Diagnostic tool for the shared-wallet isolation footgun: on ONE physical + * wallet, several virtual identities coexist, and a read must never surface a + * document scoped to another identity. When it does (identity B reading identity + * A's doc), the leak is invisible in the data — it looks like a normal read. This + * probe makes it VISIBLE: every real read/write is logged, prefixed by the ACTIVE + * identity (the discriminating virtual identity, NOT the constant physical wallet + * id), so replaying the scenario shows the exact line where a doc is accessed + * under the wrong identity. + * + * OFF by default → zero overhead, zero output. Turned on either by the SDK config + * option `debugAccessLog: true` (via {@link setAccessLog}) or, without touching + * the calling code, by the env var `NG_EVENTUALLY_ACCESS_LOG=1`. The `enabled()` + * gate is a single boolean read on the hot path when off. + * + * Polyfill-era, like the rest of /polyfill; removed at the real multi-store + * migration where the broker/verifier enforces isolation natively. + */ + +import { getCurrentUser } from "./polyfill"; + +/** Access kind: a document READ or a document WRITE. */ +export type AccessOp = "READ" | "WRITE"; + +// Config-driven toggle (set by configure() via setAccessLog); default OFF. +let configEnabled = false; + +/** + * Env override: `NG_EVENTUALLY_ACCESS_LOG=1` (or `true`) turns the log on without + * a code change in the caller. Read once, tolerant of env access throwing (e.g. + * a locked-down runtime), so it never breaks the hot path. + */ +function envEnabled(): boolean { + try { + const v = (globalThis as any)?.process?.env?.NG_EVENTUALLY_ACCESS_LOG; + return v === "1" || v === "true"; + } catch { + return false; + } +} + +/** Set the config-driven toggle (called from configure()). */ +export function setAccessLog(on: boolean): void { + configEnabled = on; +} + +/** Whether access logging is currently on (config OR env). */ +export function enabled(): boolean { + return configEnabled || envEnabled(); +} + +/** + * The identity to prefix an access line with: the ACTIVE virtual identity + * (`getCurrentUser`) — the account/space the operation is scoped under, which is + * the discriminating signal for the isolation leak. NOT the physical wallet id + * (shared, constant → useless). `(none)` when no identity is set yet (startup). + */ +function activeIdentity(): string { + return getCurrentUser() ?? "(none)"; +} + +/** + * Log one document access — but ONLY when {@link enabled}. Off → returns + * immediately, prints nothing. Format: + * `[] READ (