88914f50ae
Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement (`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est partagé — trois destins sans rapport. Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que l'`exports` du paquet et le code du consommateur ne bougent pas : - `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur, pas d'I/O. Survit comme connaissance. - `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé. - `emulated-verifier/` — les doublures de ce que le verifier fait nativement : possession, dépôt des caps, frontière, non-livraison, traitement des inbox, registres de branche, ouverture de repo. **C'est le dossier où diverger du modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de, jamais est — cette bibliothèque ne réside dans aucune couche de la cible, elle les référence. - `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de rester invisible depuis `surface/`. S'évapore, remplacé par rien. `store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la main ce que la frontière de dossier fait structurellement — c'est la meilleure preuve interne du bien-fondé de ce rangement. Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les scissions des modules à cheval suivent. 157 tests unitaires, typecheck src/test/e2e vert.
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import { escapeLiteral, escapeIri, assertNuri } from "../src/surface/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/);
|
|
});
|