43aadbeb45
98 annotations posées à côté des déclarations, et un test qui les exige sur la surface publiée. Elles portent trois choses : le niveau qui répond, la référence amont, et la catégorie parmi les cinq. La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que j'avais classé « notre invention » en raisonnant depuis l'absence, alors que c'est le meilleur alignement disponible. Les références citent un SYMBOLE, jamais une ligne : trois citations du document avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source — un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos noms, plus un argument jamais transmis. La sémantique survit à la migration, les sites d'appel non, et la nuance disparaissait sous une étiquette trop flatteuse. Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à une invention qui prétendrait citer une référence — vérifié en cassant les trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé dans index.ts se faisait analyser comme une annotation. La classification couvre l'interne qui prétend ressembler à la cible — tout emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine portait sur une fonction non exportée ; n'être pas publié n'a protégé personne. Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la seconde nommée dans la note plutôt que lissée.
172 lines
8.9 KiB
TypeScript
172 lines
8.9 KiB
TypeScript
/**
|
|
* 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).
|
|
*/
|
|
// The session id is `string | number` because that is what upstream DECLARES for it
|
|
// (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`),
|
|
// and the wasm side deserializes it as a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358`
|
|
// `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`). It only ever TRAVELS
|
|
// through this chain — never normalise it, and above all never stringify it: a JS string
|
|
// fails that deserialization, observed live as
|
|
// `Deserialization error of session_id JsValue("1")`.
|
|
// @provenance docs.docCreate kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_create — same arguments in the same order; JS-cased name, and the broker's answer is validated into a `Nuri`
|
|
export async function docCreate(
|
|
sessionId: string | number,
|
|
crdt: string,
|
|
cls: string,
|
|
dest: string,
|
|
store?: unknown,
|
|
): Promise<Nuri> {
|
|
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<Nuri>` 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).
|
|
*
|
|
* Returns what the real method returns: upstream answers the COMMITS the update
|
|
* produced (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`;
|
|
* the installed `index.d.ts:297` types it `Promise<any>`). This function already
|
|
* relayed that value at runtime — only the declared type said `void`, which threw the
|
|
* answer away for every caller. Typed `unknown` rather than `any`, exactly as
|
|
* {@link sparqlQuery} already renders the same upstream `Promise<any>`: the value is
|
|
* the broker's to shape, and a caller that ignores it is unaffected.
|
|
*/
|
|
// @provenance docs.sparqlUpdate kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_update — same arguments; the trailing `label` is a lib-internal access-log tag, never forwarded
|
|
export async function sparqlUpdate(
|
|
sessionId: string | number,
|
|
query: string,
|
|
anchorLike?: NuriLike,
|
|
label = "sparqlUpdate",
|
|
): Promise<unknown> {
|
|
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.
|
|
*/
|
|
// @provenance docs.sparqlQuery kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_query — same arguments; the trailing `label` is a lib-internal access-log tag, never forwarded
|
|
export async function sparqlQuery(
|
|
sessionId: string | number,
|
|
query: string,
|
|
base?: string,
|
|
anchorLike?: NuriLike,
|
|
label = "sparqlQuery",
|
|
): Promise<unknown> {
|
|
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;
|
|
}
|