refactor: renommer client → sdk, et fusionner les deux portes en une
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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/);
|
||||
});
|
||||
Reference in New Issue
Block a user