Chapitre 21Référence
Référence des routes
Les quatre routes : requêtes, réponses, codes, plafonds. Ce qu'elles acceptent, ce qu'elles refusent, et dans quel ordre.
Ce qui est commun aux quatre
- Réponses en JSON,
cache-control: no-store. - Toute méthode non prévue reçoit
405. - L'ordre des contrôles est le même partout : débit → taille annoncée → identité → taille reçue → forme → règles métier.
- Les messages d'erreur ne portent aucun détail technique. Le détail va dans les journaux serveur.
- Aucune réponse ne contient de secret : ni jeton, ni empreinte, ni contenu de session.
POST /api/auth
Ouvre une session à partir de la clé du site. C'est la seule route qui évalue une identité.
curl -i -X POST https://le-site.fr/api/auth \
-H "content-type: application/json" \
-d '{"key":"la-cle-du-site"}'set-cookie: inline_session=…; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=28800
set-cookie: inline_edit=1; Secure; SameSite=Strict; Path=/; Max-Age=28800
{ "ok": true }| Code | Corps | Quand |
|---|---|---|
200 | { "ok": true } | Clé correcte. Deux cookies posés. |
401 | { "error": "clé incorrecte" } | Clé fausse, absente, trop longue, corps illisible, ou configuration manquante. Le même message dans tous les cas. |
429 | { "error": "Trop de tentatives…" } | Plus de 5 tentatives par quart d'heure. En-tête retry-after. |
405 | { "error": "method_not_allowed" } | Autre méthode que POST ou DELETE. |
Coût : environ 350 ms, le temps d'argon2id. C'est voulu — c'est ce qui rend la force brute impraticable. Corps limité à 2 000 octets, clé à 256 caractères : au-delà, rien n'est calculé.
DELETE /api/auth
Ferme la session : les deux cookies sont effacés. Répond toujours 200.
GET /api/content
Lit l'état réel du dépôt et la version de référence du verrou optimiste. L'overlay l'appelle au chargement.
curl -s "https://le-site.fr/api/content?path=src/content/pages/fr/home.json" \
--cookie "inline_session=…"{
"content": "{\n \"meta\": { … }\n}\n",
"version": "3f2a1c…"
}| Code | Quand |
|---|---|
200 | Lecture réussie. content est le fichier entier, en texte. |
400 | path absent ou hors de la liste blanche. |
401 | Pas de session valide. |
404 | Le fichier n'existe pas dans le dépôt. |
429 | Plus de 60 lectures par 5 minutes. |
502 | Le dépôt est injoignable ou refuse l'accès. |
POST /api/save
Valide et publie une page. C'est la route la plus contrôlée du projet.
{
"path": "src/content/pages/fr/home.json",
"content": "{ … le fichier JSON entier, en texte … }",
"version": "3f2a1c…",
"message": "content(fr): home — hero.title"
}| Champ | Contrainte |
|---|---|
path | src/content/pages/{langue déclarée}/{page}.json, 120 caractères au plus. |
content | Le fichier entier, pas un diff. 100 Ko au plus. |
version | Celle reçue de /api/content. Vide pour un fichier neuf. 200 caractères au plus. |
message | Facultatif. Réduit à une ligne de 120 caractères, sans caractère de contrôle. |
{ "version": "9b7e42…" }| Code | Corps | Quand |
|---|---|---|
200 | { "version": … } | Écrit. La nouvelle version sert à la publication suivante. |
400 | bad_request | Corps illisible, chemin hors liste blanche, champ manquant. |
401 | unauthorized | Pas de session valide. |
409 | conflict | La version ne correspond plus : quelqu'un a publié entre-temps. |
413 | too_large | Contenu > 100 Ko, ou enveloppe > 128 Ko. |
422 | invalid_content | Schéma Zod, média invalide, identifiants en double, balisage hostile. |
429 | message d'attente | Plus de 30 publications par 5 minutes. |
502 | not_found, unauthorized, unavailable | Le dépôt a refusé ou n'a pas répondu. |
Exactement ce qui vient d'être validé et assaini, réécrit en JSON indenté. On repart de l'objet analysé et non du résultat de Zod : celui-ci supprimerait au passage toute clé qu'il ne connaît pas encore.
POST /api/upload
Reçoit une image déjà recadrée et convertie par le navigateur, la vérifie, la renomme et la range dans le dépôt.
file le fichier image (JPEG, PNG ou WebP), 20 Mo au plus
name nom souhaité, 200 caractères au plus — indicatif, toujours réécrit{ "src": "fournil-au-petit-matin.webp", "width": 1600, "height": 900 }| Code | Corps | Quand |
|---|---|---|
200 | { src, width, height } | Rangé. src est le nom réécrit, à recopier tel quel dans le contenu. |
400 | bad_request | Formulaire illisible, champ file absent, ou nom refusé après réécriture. |
401 | unauthorized | Pas de session valide. |
413 | too_large | Plus de 20 Mo, annoncés ou reçus. |
415 | unsupported_format + kind | Format non reconnu aux octets, ou dimensions hors bornes (1 à 10 000 px). kind dit ce qui a été reconnu. |
429 | message d'attente | Plus de 30 envois par quart d'heure. |
502 | code du dépôt | Écriture impossible. |
Ni le type MIME annoncé, ni le nom du fichier, ni les dimensions. Le format est reconnu à ses octets, les dimensions sont lues dans l'en-tête, le nom est réécrit — puis revérifié contre la même liste blanche que celle imposée aux références dans le contenu.
Le routeur
Un site déclare ses routes une fois, et l'hébergeur s'y branche. La déclaration tient en une ligne : la seule décision qui appartienne au site, ce sont ses langues.
import { createRouter } from 'inline-core/server';
import { LOCALES } from './locales';
export const api = createRouter({ locales: LOCALES });
createRouter renvoie les quatre routes sous trois formes. Chaque hébergeur prend
celle qui lui convient — aucune n'est privilégiée, et le paquet n'en connaît aucun.
| Forme | Pour quel hébergeur |
|---|---|
api.routes['/api/save'] | celui qui découvre les routes par l'arborescence et attend des exports nommés |
api.handle(request, env) | tous les autres : un point d'entrée unique, qui choisit la route et la méthode |
api.find(pathname) | la route servant un chemin, ou undefined — de quoi séparer l'API du statique dans un serveur maison |
env porte ce que l'hébergeur expose : les variables, et les liaisons éventuelles
— dont RATE_LIMIT. Une méthode non servie reçoit un 405, un chemin inconnu un
404, et toujours en JSON : l'appelant est l'overlay, une page d'erreur ne lui apprend rien.
Avant, chaque site rangeait les quatre fabriques dans quatre fichiers écrits à la convention d'un hébergeur précis. La répartition — quel chemin, quelle méthode, quelle réponse — vit désormais dans le paquet et se met à jour avec lui.
Les fabriques
Elles restent exportées, pour un câblage à la main : createRouter ne fait que
les réunir. Un site en 2.0.x n'a donc rien à changer.
import {
createAuthRoute, // () → onRequest, onRequestPost, onRequestDelete
createContentRoute, // ({ locales }) → onRequest, onRequestGet
createSaveRoute, // ({ locales }) → onRequest, onRequestPost
createUploadRoute, // () → onRequest, onRequestPost
} from 'inline-core/server';
SiteConfig ne contient qu'une chose : locales. Si cette interface
s'allonge, c'est le signe qu'une décision du paquet a fuité vers les sites.
Les budgets de débit
| Route | Budget | Fenêtre | Ce qu'il protège |
|---|---|---|---|
/api/auth | 5 | 15 min | la clé du site |
/api/content | 60 | 5 min | le quota de l'API du dépôt |
/api/save | 30 | 5 min | le dépôt, le quota |
/api/upload | 30 | 15 min | le dépôt, le quota |
Les plafonds
| Plafond | Valeur |
|---|---|
| Contenu d'une page | 100 000 octets |
| Enveloppe d'une requête JSON | 128 000 octets |
Corps de /api/auth | 2 000 octets |
| Longueur d'une clé | 256 caractères |
| Fichier envoyé | 20 Mo |
| Dimensions d'image | 1 à 10 000 px |
| Longueur d'un chemin | 120 caractères |
| Message de publication | 120 caractères, une ligne |
| Durée de session | 8 heures |