/** * Low-level document + SPARQL primitives. * * These call the real injected `ng` (`getConfig().ng`) directly — never the * public `ng` proxy (`makeNg`). This is a validated hard constraint, not a style * choice: the public `ng` is a JS `Proxy` over `@ng-org/web`'s iframe-RPC proxy, * and layering our Proxy on top breaks `doc_create`'s `postMessage` marshaling * with **`DataCloneError: function ... could not be cloned`** — the footgun this * rule exists to prevent. Reaching the real `ng` held in the config avoids the * double-proxy. Do not import from `./ng-proxy`. * * Signatures mirror the real `@ng-org/web` `ng` surface (verified against the * app's storeRegistry usage), so this is a drop-in for those raw calls. */ import { getCaps, getConfig } from "../shared-wallet/bootstrap"; import { logAccess, enabled as accessLogEnabled } from "../shared-wallet/access-log"; import { isNuri, toNuri } from "../model/nuri"; import { assertMayReach, assertMayWrite } from "../emulated-verifier/reach"; import { fetchReadCap } from "../emulated-verifier/public-store"; import type { Nuri, NuriLike } from "../model/types"; // The low common point for ALL document access: every read in the SDK routes // through `sparqlQuery`, every write through `sparqlUpdate` (+ container creation // through `docCreate`) — each ultimately calling the real injected `ng` here. The // access log is therefore instrumented HERE so no access path escapes it. Callers // pass a semantic `label` (readDoc|readUnion|listMyEntityDocs|writeEntity|deposit // |…); it is a lib-internal probe param, NOT forwarded to the real `ng` (the docs // primitives forward the exact SDK signature — see test/docs.test.ts). When the // log is OFF (default) the extra param is inert and costs one boolean read. /** Count rows in a raw SPARQL SELECT result, tolerant of the possible shapes. */ function rowCount(result: unknown): number { if (!result) return 0; if (Array.isArray(result)) return result.length; const anyRes = result as { results?: { bindings?: unknown[] } }; return anyRes.results?.bindings?.length ?? 0; } /** * Create one document → its NURI. * * Mirrors `ng.doc_create(session_id, crdt, cls, dest, store_repo?)`. For a graph * document in the (shared) private store: `docCreate(sid, "Graph", "data:graph", * "store")` (store_repo left undefined → private store). */ export async function docCreate( sessionId: string, crdt: string, cls: string, dest: string, store?: unknown, ): Promise { const { ng } = getConfig(); const nuri = await ng.doc_create(sessionId, crdt, cls, dest, store); // The BROKER boundary. `ng` is a permissive property bag (`NgLike`), so what // comes back is `any` and this function's `Promise` would otherwise be an // unchecked promise — every typed NURI downstream rests on it. Validate once, // here, rather than let a non-reference propagate as a document. if (typeof nuri !== "string" || !isNuri(nuri)) { throw new Error( `[ng-eventually] docCreate: the broker returned something that is not a NextGraph reference: ${JSON.stringify(nuri)}`, ); } // **Creating a document gives you its cap.** Upstream that is not a courtesy but // the mechanism: `doc_create` commits `AddRepo { read_cap }` to the store's Store // branch, so the creator holds it from the first instant. Without this, a caller // could create a document through this primitive and then be refused reading or // writing it — which is what the e2e run against the live broker exposed. // // `physical.ts`'s counterpart deliberately does NOT do this: the shim's own // documents belong to no virtual user, and `store-registry` files their caps // itself, where it knows whose they are. getCaps().mint(nuri); // A container creation is a WRITE; the NURI only exists after the call. logAccess("WRITE", nuri, "docCreate"); return nuri; } /** * Run a SPARQL UPDATE (INSERT/DELETE DATA, etc.). * * Mirrors `ng.sparql_update(session_id, query, anchor?)`, where `anchor` is the * document NURI the update is scoped/base'd to (optional). */ export async function sparqlUpdate( sessionId: string, query: string, anchorLike?: NuriLike, label = "sparqlUpdate", ): Promise { const { ng } = getConfig(); const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlUpdate"); // The boundary, in two questions that are NOT the same one. // // Reaching is possession. Writing is OWNERSHIP — upstream the right to write is // membership of the repo (`verify_permission`, reachable only from `Commit::verify`, // so on commits and never on reads), and how you came by the READ key changes nothing // about it. A public store hands its read cap to whoever asks; a cap deposited in your // inbox is a Link someone gave you. Neither makes you a member. // // NO public-store fetch here, unlike the read below: asking the network for a read key // has no bearing on a write. if (anchor !== undefined) { assertMayReach(anchor, "docs.sparqlUpdate"); await assertMayWrite(anchor, "docs.sparqlUpdate"); } // `label` is a lib-internal access-log tag, NOT forwarded to `ng`. logAccess("WRITE", anchor ?? "(no anchor)", label); return ng.sparql_update(sessionId, query, anchor); } /** * Run a SPARQL SELECT/CONSTRUCT/ASK query → the raw SDK result. * * Mirrors `ng.sparql_query(session_id, query, base?, anchor?)`. `base` is the * query base IRI (usually `undefined`); `anchor` is the document NURI to query. */ export async function sparqlQuery( sessionId: string, query: string, base?: string, anchorLike?: NuriLike, label = "sparqlQuery", ): Promise { const { ng } = getConfig(); const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlQuery"); // The boundary: an ANCHORED read may only touch what the connected virtual user // reaches. An anchorless query spans the local union — a different problem (it is // O(wallet size), and the read path never uses it), not one this guard can bound. // // Asking the (emulated) network first, as `ensureRepoOpen` does: a document in a // public store gives its cap to whoever asks, and this is a door an application can // reach with nothing but a bare reference. Inert once the cap is held, memoised // otherwise — see public-store.ts. if (anchor !== undefined) { await fetchReadCap(anchor); assertMayReach(anchor, "docs.sparqlQuery"); } // `label` is a lib-internal access-log tag, NOT forwarded to `ng`. const result = await ng.sparql_query(sessionId, query, base, anchor); // Log AFTER the read so the row count (a strong leak signal: a doc rendering // rows under an identity that should see nothing) can be appended. Skip the // rowCount work entirely when the log is off. if (accessLogEnabled()) { // `rows` here are raw RDF triple bindings (the SPARQL `?s ?p ?o` result), NOT // domain objects — one document's entity is spread across several triple rows. // Spell that out so the log isn't mistaken for an object count (the app-level // object/shape count is logged separately by useShapeQuery → dataStats). logAccess("READ", anchor ?? "(no anchor)", label, " → " + rowCount(result) + " triple-rows"); } return result; }