Files
Sylvain Duchesne 63ecfeeff8 docs+refactor(client): fidelity pass — id identity, drop connections, no faux-login, accurate NextGraph framing
Align the polyfill's surface and docs with the verified NextGraph reality and
remove application-level concepts:

- Identity is an ID, not a username: AccountRecord.id, shim predicate shim:id,
  normalizeId; accounts core becomes IdentityStore (set/clear/get) — the faux
  login/logout framing is gone (identity is set at wallet-import time).
- Relationship/connection is an application concept, not a platform primitive
  (NextGraph has no bilateral-connection primitive: grantee is unpersisted
  scaffolding, cap-send is unimplemented). Remove connections.ts; caps exposes
  only a directed grantRead(doc, granteeId) + a read-only protectedDocsOf(owner).
  Delete the now-dead isolation.ts social-visibility axis.
- Inbox docs: NextGraph has no separate curator — the recipient's own verifier
  unseals and applies each queued sealed message inline (process_inbox);
  inbox_post_link is a proposed/future API. Stop attributing the emulated
  curator to the platform.
- Read isolation reframed around the outcome: no cap -> empty union read;
  targeted read of an unheld repo -> RepoNotFound; cap introspection
  (canRead/governsRead) is emulation-only with no NextGraph API behind it.
- read-model.md corrected: the listing path is per-doc ANCHORED default-graph
  queries, never the anchorless GRAPH ?g union (that is O(wallet)); the probe
  section no longer claims the opposite.
- README recap table restructured (target | current NextGraph status | current
  emulation); INDEX_ACCOUNT documented as reservedAccount("index") in the
  sentinel namespace; de-domained generic-layer comments; softened tone.

Consumer application (Festipod) rewired separately to own the relationship
concept and feed the lib an id. Lib gates: bun test 83 pass / 0 fail, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:02:16 +02:00

79 lines
3.1 KiB
TypeScript

import { test, expect } from "bun:test";
import { escapeLiteral, escapeIri, assertNuri } from "../src/sparql";
// --- escapeLiteral --------------------------------------------------------
test("escapeLiteral escapes backslash, quote and whitespace controls", () => {
expect(escapeLiteral('a"b')).toBe('a\\"b');
expect(escapeLiteral("a\\b")).toBe("a\\\\b");
expect(escapeLiteral("a\nb")).toBe("a\\nb");
expect(escapeLiteral("a\rb")).toBe("a\\rb");
expect(escapeLiteral("a\tb")).toBe("a\\tb");
});
test("escapeLiteral backslash-then-quote order does not double-escape", () => {
// `\` first so a raw `"` never becomes `\"` before the quote pass mangles it.
expect(escapeLiteral('\\"')).toBe('\\\\\\"');
});
test("escapeLiteral output can no longer close a SPARQL literal", () => {
const injected = '" ; <urn:evil> "pwn';
const escaped = escapeLiteral(injected);
// No RAW double-quote survives — every `"` is preceded by a backslash.
expect(/(^|[^\\])"/.test(escaped)).toBe(false);
});
// --- escapeIri ------------------------------------------------------------
test("escapeIri passes ordinary printable identifier chars through unchanged", () => {
expect(escapeIri("alice")).toBe("alice");
expect(escapeIri("a.b-c_d:e/f")).toBe("a.b-c_d:e/f");
});
test("escapeIri percent-encodes every IRI-breaking character", () => {
expect(escapeIri("a>b")).toBe("a%3Eb");
expect(escapeIri("a<b")).toBe("a%3Cb");
expect(escapeIri('a"b')).toBe("a%22b");
expect(escapeIri("a b")).toBe("a%20b");
expect(escapeIri("a\nb")).toBe("a%0Ab");
expect(escapeIri("a\tb")).toBe("a%09b");
expect(escapeIri("a\\b")).toBe("a%5Cb");
});
test("escapeIri neutralises a full breakout attempt", () => {
const attack = 'x> <urn:evil> "pwn';
const encoded = escapeIri(attack);
// The encoded id cannot contain a raw `>`, `<`, `"` or space, so it
// cannot escape the surrounding <PREFIX:...> IRI.
expect(encoded).not.toMatch(/[<>" ]/);
});
test("escapeIri handles unicode without corrupting it (round-trips via decode)", () => {
const u = "élan";
// "é" is a printable letter → left as-is; a space would be encoded.
expect(escapeIri(u)).toBe("élan");
expect(escapeIri("é ")).toBe("é%20");
});
// --- assertNuri -----------------------------------------------------------
test("assertNuri returns valid NURIs unchanged", () => {
expect(assertNuri("did:ng:o:doc1")).toBe("did:ng:o:doc1");
expect(assertNuri("urn:ng-eventually:shim")).toBe("urn:ng-eventually:shim");
expect(assertNuri("did:ng:PRIV")).toBe("did:ng:PRIV");
});
test("assertNuri throws on empty / non-string", () => {
expect(() => assertNuri("")).toThrow(/invalid NURI/);
// @ts-expect-error deliberately wrong type
expect(() => assertNuri(null)).toThrow(/invalid NURI/);
});
test("assertNuri throws on IRI-breaking characters", () => {
expect(() => assertNuri("did:ng:o> <urn:evil")).toThrow(/IRI-forbidden/);
expect(() => assertNuri('did:ng:"x')).toThrow(/IRI-forbidden/);
expect(() => assertNuri("did:ng: x")).toThrow(/IRI-forbidden/);
expect(() => assertNuri("did:ng:\nx")).toThrow(/IRI-forbidden/);
expect(() => assertNuri("did:ng:\tx")).toThrow(/IRI-forbidden/);
});