feat(client): inbox mechanism, write-guard, SPARQL injection hardening

Polyfill capabilities landed for Festipod's T02 features (all generic,
zero-domain — the consumer injects the domain).

- inbox: implement the previously-stubbed namespace. post(target,{from?,
  payload,ts?}) deposits {from,payload,ts} as RDF via docs.sparqlUpdate (the
  real injected ng, never makeNg); read/materialize + watch emulate the curator
  in-lib (deposits read via docs.sparqlQuery). `from` optional = anonymity.
- write-guard: caps.hasWritePolicy() + ng-proxy.sparql_update rejects when the
  target doc is under a write policy and the current user lacks its write cap;
  passthrough otherwise (no regression). Read-cap registry unchanged.
- sparql.ts (new): escapeLiteral / escapeIri / assertNuri, exported from index.
  store-registry now escapes every literal and validates/encodes every IRI-
  position value — closes a SPARQL-injection hole where an untrusted username
  could inject triples into the shim (the account→doc-NURI trust root).

Tests: inbox, sparql (incl. injection), ng-proxy write-guard, isolation-active.
68 tests pass; tsc --noEmit rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-03 15:51:00 +02:00
parent 654cb90d99
commit d804a436d7
12 changed files with 924 additions and 42 deletions
+22 -3
View File
@@ -4,7 +4,8 @@
* surface stays identical to `@ng-org/web`'s `ng`.
*/
import { getConfig } from "./polyfill";
import { getConfig, getCaps, getCurrentUser } from "./polyfill";
import type { Nuri } from "./types";
export function makeNg(): Record<string, any> {
return new Proxy({} as Record<string, any>, {
@@ -21,10 +22,28 @@ export function makeNg(): Record<string, any> {
}
// sparql_update → write guard (emulated write-cap check).
// Mirrors the target broker/verifier: a write is refused unless the wallet
// holds the document's WRITE cap. Emulated per-document via CapRegistry.
// args = (session_id, query, anchor?) — `anchor` is the target doc NURI.
if (prop === "sparql_update") {
return (...args: any[]) => {
// TODO(polyfill): reject when getCurrentUser() lacks the write grant
// on the target document (see access.canWrite). For now, passthrough.
const anchor = args[2] as Nuri | undefined;
const caps = getCaps();
// Passthrough (no regression) unless a WRITE policy exists AND this
// specific document is governed by it. Ungoverned docs (mono-store
// default, no cap declared) flow through exactly as before.
if (
typeof anchor === "string" &&
caps.hasWritePolicy() &&
caps.governsWrite(anchor) &&
!caps.canWrite(anchor, getCurrentUser())
) {
return Promise.reject(
new Error(
`[ng-eventually] write denied: current user lacks the write cap for ${anchor}`,
),
);
}
return ng.sparql_update!(...args);
};
}