refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph

Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont
ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever
l'ambiguïté à chaque phrase — et le contrat publié, lu par une application,
était le pire endroit pour laisser traîner ça.

packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill,
contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts →
e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md.

Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes,
y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est
fait occurrence par occurrence, pas par substitution.

Le contrat énonce désormais son identité en une phrase : « This package is a
polyfill of NextGraph's SDK. »
This commit is contained in:
Sylvain Duchesne
2026-08-10 17:14:25 +02:00
parent 49b046268e
commit 737729c9ce
88 changed files with 122 additions and 106 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* SPARQL string-building safety helpers — shared by every module that builds
* SPARQL by interpolation (inbox, store-registry).
*
* These exist because of SPARQL injection. When an untrusted value (an identity
* id, a payload) is spliced verbatim into a query, a `"` closes a literal and a
* `>` closes an IRI, letting the value inject arbitrary triples (or corrupt the
* shim graph, the trust root mapping accounts → document NURIs). Every value that
* reaches a query passes through one of these helpers first.
*
* Two positions, two strategies:
* - Literal position (`"..."`): {@link escapeLiteral}. Escape rather than reject,
* because literals legitimately carry arbitrary text (JSON payloads, display
* names). Escaping is lossless and reversible.
* - IRI position (`<...>`): two cases.
* · Trusted-shaped NURIs coming back from `ng` (`did:ng:...`): validate with
* {@link assertNuri} — they should never contain IRI-breaking chars; if one
* does, something upstream is wrong, so it throws rather than silently
* building a broken/injected query.
* · Untrusted values embedded into an IRI (an identity id used to mint an
* account-subject IRI): {@link escapeIri} percent-encodes every IRI-hostile
* character. Encode rather than reject so any id (spaces, unicode,
* punctuation) stays usable, while `<`, `>`, `"`, whitespace and control
* chars can never break out of the IRI.
*/
/**
* Escape a value for embedding inside a SPARQL string literal (`"..."`).
* Escapes backslash, double-quote and the C0 whitespace controls that would
* otherwise terminate or corrupt the literal. Lossless / reversible.
*/
export function escapeLiteral(value: string): string {
return value
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t");
}
/**
* Delimiter characters that must never appear raw inside a SPARQL/Turtle IRI
* ref (`<...>`): the space plus `< > " { } | ^ backtick \`. Whitespace beyond
* the space and all C0/C1 control characters are handled by the code-point
* check in {@link isIriForbidden}. Any of these would let a value break out of
* the `<...>` and inject arbitrary syntax.
*/
const IRI_FORBIDDEN_DELIMS = /[<>"{}|^`\\ ]/;
/** True if `ch` (a single code point) may not appear raw inside an IRI ref. */
function isIriForbidden(ch: string): boolean {
const code = ch.codePointAt(0)!;
return IRI_FORBIDDEN_DELIMS.test(ch) || code < 0x20 || code === 0x7f;
}
/**
* Percent-encode every IRI-hostile character in `value` so it is safe to embed
* inside a SPARQL IRI ref (`<PREFIX:${escapeIri(value)}>`). Use this for
* untrusted values (e.g. an identity id minted into an account-subject IRI):
* encoding keeps every id usable while making breakout impossible.
*
* NOTE: this encodes only the delimiter/whitespace/control set, so ordinary
* printable characters (including `:` `/` `.` `-` `_` and unicode letters) pass
* through unchanged and the resulting IRI stays human-readable.
*/
export function escapeIri(value: string): string {
let out = "";
for (const ch of value) {
if (isIriForbidden(ch)) {
// Percent-encode each UTF-8 byte of the offending character. Also encode
// the chars encodeURIComponent leaves alone but which are IRI-hostile.
out += encodeURIComponent(ch).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
} else {
out += ch;
}
}
return out;
}
/**
* Assert that `nuri` is safe to embed verbatim inside a SPARQL IRI ref. NURIs
* that come back from `ng` are trusted-SHAPED (`did:ng:...` or `urn:...`) and
* should never carry IRI-breaking characters; if one does, we throw rather than
* emit a query that could be malformed or injected. Returns the value unchanged
* so it can be used inline: `<${assertNuri(doc)}>`.
*
* Generic in its argument so the caller's type flows THROUGH: passing a `Nuri`
* gives back a `Nuri`, not a widened `string`. This function checks characters,
* not the `did:ng:` shape (it legitimately accepts `urn:…` IRIs too), so it must
* not be the thing that mints a `Nuri` — that is {@link isNuri}'s job.
*/
export function assertNuri<T extends string>(nuri: T): T {
if (typeof nuri !== "string" || nuri.length === 0) {
throw new Error(`[sparql] invalid NURI (empty): ${JSON.stringify(nuri)}`);
}
for (const ch of nuri) {
if (isIriForbidden(ch)) {
throw new Error(
`[sparql] NURI contains IRI-forbidden characters, refusing to embed: ${JSON.stringify(nuri)}`,
);
}
}
return nuri;
}