EN FR

Chapter 21Reference

Route reference

The four routes: requests, responses, codes, caps. What they accept, what they refuse, and in which order.

5 min read10 sectionsChapter 21 / 22

What the four share

  • JSON responses, cache-control: no-store.
  • Any unexpected method gets 405.
  • The order of checks is the same everywhere: rate → declared size → identity → received size → shape → business rules.
  • Error messages carry no technical detail. Detail goes to the server logs.
  • No response contains a secret: no token, no hash, no session content.

POST /api/auth

Opens a session from the site key. It is the only route that evaluates an identity.

request bash
curl -i -X POST https://the-site.com/api/auth \
  -H "content-type: application/json" \
  -d '{"key":"the-site-key"}'
response — 200 text
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 }
CodeBodyWhen
200{ "ok": true }Correct key. Two cookies set.
401{ "error": "clé incorrecte" }Wrong key, missing, too long, unreadable body, or missing configuration. The same message in every case.
429{ "error": "Trop de tentatives…" }More than 5 attempts per quarter of an hour. retry-after header.
405{ "error": "method_not_allowed" }Any method other than POST or DELETE.

Cost: about 350 ms, the time argon2id takes. Deliberate — it is what makes brute force impractical. Body capped at 2,000 bytes, key at 256 characters: beyond that, nothing is computed.

DELETE /api/auth

Closes the session: both cookies are cleared. Always answers 200.

GET /api/content

Reads the repository's real state and the reference version for the optimistic lock. The overlay calls it on load.

request bash
curl -s "https://the-site.com/api/content?path=src/content/pages/en/home.json" \
  --cookie "inline_session=…"
response — 200 json
{
  "content": "{\n  \"meta\": { … }\n}\n",
  "version": "3f2a1c…"
}
CodeWhen
200Read succeeded. content is the whole file, as text.
400path missing or outside the whitelist.
401No valid session.
404The file does not exist in the repository.
429More than 60 reads per 5 minutes.
502The repository is unreachable or refuses access.

POST /api/save

Validates and publishes a page. It is the most controlled route in the project.

expected body json
{
  "path": "src/content/pages/en/home.json",
  "content": "{ … the whole JSON file, as text … }",
  "version": "3f2a1c…",
  "message": "content(en): home — hero.title"
}
FieldConstraint
pathsrc/content/pages/{declared language}/{page}.json, 120 characters max.
contentThe whole file, not a diff. 100 KB max.
versionThe one received from /api/content. Empty for a new file. 200 characters max.
messageOptional. Reduced to a single 120-character line, no control characters.
response — 200 json
{ "version": "9b7e42…" }
CodeBodyWhen
200{ "version": … }Written. The new version is used by the next publish.
400bad_requestUnreadable body, path outside the whitelist, missing field.
401unauthorizedNo valid session.
409conflictThe version no longer matches: someone published in the meantime.
413too_largeContent > 100 KB, or envelope > 128 KB.
422invalid_contentZod schema, invalid media, duplicate ids, hostile markup.
429wait messageMore than 30 publishes per 5 minutes.
502not_found, unauthorized, unavailableThe repository refused or did not answer.
What gets written

Exactly what was just validated and sanitised, re-serialised as indented JSON. It starts from the parsed object rather than Zod's output: the latter would strip any key it does not know yet.

POST /api/upload

Receives an image already cropped and converted by the browser, checks it, renames it and stores it in the repository.

body — multipart/form-data text
file   the image file (JPEG, PNG or WebP), 20 MB max
name   desired name, 200 characters max — indicative, always rewritten
response — 200 json
{ "src": "bakehouse-at-dawn.webp", "width": 1600, "height": 900 }
CodeBodyWhen
200{ src, width, height }Stored. src is the rewritten name, to be copied as-is into the content.
400bad_requestUnreadable form, missing file field, or name refused after rewriting.
401unauthorizedNo valid session.
413too_largeMore than 20 MB, declared or received.
415unsupported_format + kindFormat not recognised from the bytes, or dimensions out of range (1 to 10,000 px). kind says what was recognised.
429wait messageMore than 30 uploads per quarter of an hour.
502repository codeWrite impossible.
Nothing declared is believed

Not the announced MIME type, not the file name, not the dimensions. The format is recognised from its bytes, dimensions are read from the header, the name is rewritten — then re-checked against the same whitelist imposed on content references.

The router

A site declares its routes once, and the host plugs into them. The declaration is one line: the only decision that belongs to the site is its locales.

src/lib/api.ts ts
import { createRouter } from 'inline-core/server';
import { LOCALES } from './locales';

export const api = createRouter({ locales: LOCALES });

createRouter returns the four routes in three shapes. Each host takes the one it needs — none is preferred, and the package knows about none of them.

ShapeFor which host
api.routes['/api/save']one that discovers routes from a file tree and expects named exports
api.handle(request, env)every other one: a single entry point that picks the route and the method
api.find(pathname)the route serving a path, or undefined — enough to split API from static in a server of your own

env carries whatever the host exposes: the variables, and any bindings — including RATE_LIMIT. An unserved method gets a 405, an unknown path a 404, and always as JSON: the caller is the overlay, and an error page teaches it nothing.

Since version 2.1.0

Before, each site kept the four factories in four files written to one host's convention. Dispatch — which path, which method, which response — now lives in the package and is updated with it.

The factories

They are still exported, for wiring by hand: createRouter only gathers them. A site on 2.0.x has nothing to change.

ts ts
import {
  createAuthRoute,      // ()                     → onRequest, onRequestPost, onRequestDelete
  createContentRoute,   // ({ locales })          → onRequest, onRequestGet
  createSaveRoute,      // ({ locales })          → onRequest, onRequestPost
  createUploadRoute,    // ()                     → onRequest, onRequestPost
} from 'inline-core/server';

SiteConfig holds one thing: locales. If that interface grows, it is a sign a package decision has leaked into the sites.

Rate budgets

RouteBudgetWindowWhat it protects
/api/auth515 minthe site key
/api/content605 minthe repository API quota
/api/save305 minthe repository, the quota
/api/upload3015 minthe repository, the quota

Caps

CapValue
Page content100,000 bytes
JSON request envelope128,000 bytes
/api/auth body2,000 bytes
Key length256 characters
Uploaded file20 MB
Image dimensions1 to 10,000 px
Path length120 characters
Commit message120 characters, one line
Session lifetime8 hours