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:
@@ -185,7 +185,7 @@ export async function loadShim(): Promise<Map<string, AccountRecord>> {
|
||||
}`;
|
||||
const map = new Map<string, AccountRecord>();
|
||||
try {
|
||||
const result = await sparqlQuery(s.sessionId, query, undefined, anchor);
|
||||
const result = await sparqlQuery(s.sessionId, query, undefined, anchor, "loadShim");
|
||||
for (const row of readBindings(result)) {
|
||||
const id = bindingValue(row, "id");
|
||||
if (!id) continue;
|
||||
@@ -238,7 +238,7 @@ export async function resolveAccount(id: string): Promise<AccountRecord | null>
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
const result = await sparqlQuery(s.sessionId, query, undefined, anchor);
|
||||
const result = await sparqlQuery(s.sessionId, query, undefined, anchor, "resolveAccount");
|
||||
const rows = readBindings(result);
|
||||
if (rows.length === 0) return null;
|
||||
const row = rows[0]!;
|
||||
@@ -305,7 +305,7 @@ export async function ensureAccount(id: string): Promise<AccountRecord> {
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await sparqlUpdate(s.sessionId, update, anchor);
|
||||
await sparqlUpdate(s.sessionId, update, anchor, "ensureAccount");
|
||||
} catch (error) {
|
||||
console.error("[storeRegistry] ensureAccount persist failed:", error);
|
||||
}
|
||||
@@ -432,6 +432,7 @@ export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri> {
|
||||
// a literal → escapeLiteral.
|
||||
`INSERT DATA { <${INDEX_SUBJECT}> <${P.contains}> "${escapeLiteral(entityNuri)}" }`,
|
||||
indexDoc,
|
||||
"createEntityDoc",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[storeRegistry] createEntityDoc index append failed:", error);
|
||||
@@ -451,6 +452,7 @@ async function readScopeIndex(indexDoc: Nuri): Promise<Nuri[]> {
|
||||
`SELECT ?e WHERE { <${INDEX_SUBJECT}> <${P.contains}> ?e }`,
|
||||
undefined,
|
||||
indexDoc,
|
||||
"readScopeIndex",
|
||||
);
|
||||
for (const row of readBindings(res)) {
|
||||
const v = bindingValue(row, "e");
|
||||
|
||||
Reference in New Issue
Block a user