Files
ng-eventually/packages/client/src/ng-proxy.ts
T
Sylvain Duchesne d804a436d7 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>
2026-07-03 15:51:00 +02:00

60 lines
2.3 KiB
TypeScript

/**
* The wrapped `ng`: a Proxy that forwards every method to the real SDK and
* overrides only what the broker/verifier will do natively at migration. The
* surface stays identical to `@ng-org/web`'s `ng`.
*/
import { getConfig, getCaps, getCurrentUser } from "./polyfill";
import type { Nuri } from "./types";
export function makeNg(): Record<string, any> {
return new Proxy({} as Record<string, any>, {
get(_target, prop: string) {
const { ng } = getConfig();
// login / session_start → open the SHARED wallet invisibly.
if (prop === "login" || prop === "session_start") {
return (...args: any[]) => {
// TODO(polyfill): supply shared-wallet credentials so no wallet UI
// is shown. For now, passthrough.
return ng[prop]!(...args);
};
}
// 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[]) => {
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);
};
}
// TODO(anticipated API): inbox_post_link + capability operations — expose
// here with their anticipated signatures, emulated for now.
// Everything else: passthrough to the real SDK, unchanged.
const real = ng[prop];
return typeof real === "function" ? real.bind(ng) : real;
},
});
}