refactor(api): précis en sortie, permissif en entrée — plus de guard publié

Un polyfill ne doit rien faire de plus que ce qui est prévu. `isNuri` /
`hasReadCap` et les utilitaires SPARQL `escapeLiteral` / `escapeIri` /
`assertNuri` n'ont de pendant à aucun niveau et n'en auront pas : le binding
prend `nuri: String`, le moteur est fortement typé en Rust et n'a besoin
d'aucun prédicat, l'ORM n'expose rien de tel. Le contrat les justifiait parce
qu'ils « restent utiles à n'importe quelle app » — c'est exactement le
raisonnement à refuser : utile n'est pas prévu, et chacun serait un appel à
réécrire le jour du SDK.

Le besoin d'un guard venait de notre propre signature : les entrées publiques
exigeaient `Nuri`, donc un consommateur devait narrower ce qu'il lisait d'une
URL ou du stockage. Elles prennent désormais `NuriLike` — n'importe quelle
chaîne — et valident à l'intérieur (`toNuri`). Ce que la bibliothèque REND
reste typé `Nuri` : l'app en profite gratuitement, et un type plus large ne
cassera rien quand le SDK rendra des chaînes.

Les guards et les utilitaires restent, internes, là où la validation se fait.

Un défaut introduit puis corrigé en chemin, qui valait le test qu'il a produit :
`readUnion` a toujours toléré les trous dans sa liste — un index de scope peut
porter une entrée blanche, et un appelant qui assemble depuis des valeurs
optionnelles n'a pas à compacter. Valider AVANT de filtrer a transformé cette
tolérance en exception. Vide est une absence, pas une référence malformée ; les
deux sont désormais distingués par un test.

170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la
bibliothèque, l'exemple et le harnais.
This commit is contained in:
Sylvain Duchesne
2026-08-06 10:51:36 +02:00
parent 54f8389e9e
commit ebf866b1f2
11 changed files with 96 additions and 35 deletions
+3 -3
View File
@@ -31,7 +31,6 @@ import {
docs,
ensureIdentity,
inbox,
isNuri,
readUnion,
storeRegistry,
subscribeDoc,
@@ -121,10 +120,11 @@ async function myNotes(scope: Scope): Promise<Note[]> {
* Read someone else's note from its link.
*
* The link is what circulates in this model — you do not discover a note, you are given
* its link. `hasReadCap` is the door an untrusted string goes through.
* its link. It arrives as a plain string, from a field or a URL, and goes straight in:
* the library validates it. Nothing to narrow, nothing to cast, and nothing that will
* have to change when the real SDK takes that same string.
*/
async function readSharedNote(link: string): Promise<Note | null> {
if (!isNuri(link)) return null;
const [note] = await readUnion([link]);
if (!note) return null;
return {