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:
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user