The site's HTTP API. Lichen-cms's two big handler files (cms.rs, router.rs) became an api/ module by topic, and the routes moved to one scheme under /cms/api: JSON by default, with ?format=html only where a page exists. Every route now sits in a permission group. The API also gained file history, a preview of the site with its drafts, batch commits, map rooms and raw file bytes.
AST-verified changes
- Item 6 step 1, the api-layer move (7601a57c): lichen-cms's HTTP layer went into
api/by topic.cms.rsandrouter.rswere emptied and deleted, and the oldapi.rsplus the handler parts of history, preview, publish_record, share and room_sync becameapi/{api_utils,build,files,frontend,history,html,live,mod,public,publish,share,site,test_routes}.rs.batch.rsbecameapi/batch.rs. Checked by the builder: "3035 items before and after, 146 moved, 25 visibility changes as mapped, 3690 comments compared, equal".verify.shrun FUHHLT: "VERIFY OK (ast 3035 items/146 moved/25 visibility/3690 comments; routes 130=130 ...)", and its planted fault (run tVqHJG) went RED. Later VERIFY OK runs: TRNKZ7, S1xZAJ, FZ0qAX, 1wrz6l, 0I9ss7. Adversary: SATISFIED, its run eMUcJc VERIFY OK, plant ip43fF RED. Allowed: path rewrites (crate::cms/router→crate::api), dropped file attributes on 4 files, member visibility, lone trailing commas. The commit was move.py plus cargo fix, cargo fmt and 2 hand edits, listed in the commit.api/html.rsandapi/frontend.rsare in #34. - OPERATOR-REVIEW-FIXES U1 (1d6fe2d4, f6a10c32): 48 items from
api/files.rs,api/html.rs,filetree.rs,history.rsandpreview.rsmoved intoapi/{api_utils,filetree,history,preview}.rs, andapi/mod.rswas renamedapi.rs. One check over 8b30050a..f6a10c32 (run id not recorded): "3004 items, 48 moved, 69 carried, 19 renamed, 2 visibility as mapped, 233 module declarations". The adversary's rerun, notify-abab368a, got the same counts. Allowed:pub(super)onis_file_hashandformat_error_chain; the text rewriteapi/mod.rs→api.rs.
Roughly 1,166 of this PR's 5,760 changed lines are code these moves carried verbatim: old cms.rs and router.rs code that reappears under api/. The routes, groups and new topics around it are new code.
On this forge the renamed file here shows as a whole deletion and a whole addition, so its diff reads 6,168 changed lines; with rename detection (git diff -M full/18-site-frontends full/19-site-api) it is 5,760.
What changed and why:
- One URL scheme (item 6, steps 1–4). The handlers moved first, as a move only (above). Then the routes were renamed under
/cms/api/…(files,folders,rename,build,render,edit,filetree,settings,events) and the old/cms/put-file,/cms/filetree,/cms/sseand their kin were removed.api_utils.rsholds the one error shape (ApiError) andrespond(), which answers a route's HTML view for?format=html. Routes with no HTML view answer?format=htmlwith the API's "no HTML" error (refuse_html,json_only). One path-encoding rule covers every route that takes a site path. - Permission groups (LICHEN-SANDBOX L2d). The route table in
api.rsis built fromsite_group(READS)andsite_group(WRITES), each admitted by the site's policy.cms_private_router_withlets a serving server add its own site routes into the same table (item 11). The front server admits a site's publish and its git editor routes ahead of the site's (front_site_routes, SITE-FRONT-BOUNDARY 9a), and since FULL-SITE-PROCESS (6d, 6e) forwards them to the site process's admin thread. The push permissions in its groups are now the rights to ask for a push (TriggerPushToRemote,TriggerPushToPds, O4), apart from the credentials.GET /cms/api/healthis public, for the front server's check that a site process is up. - New topics:
history.rs:GET /cms/api/historywith four questions — a file's commits, its bytes at a commit, the files deleted under a folder, the files changed since a commit. Every git command is confined to the site, with literal pathspecs.batch.rs:POST /cms/api/batch, several writes as one commit. Every operation is checked before any is applied, and the commit holds only the batch's paths.preview.rs:GET /cms/api/preview/{*url}andPOST /cms/api/build/check.live.rs: text and map rooms side by side, with their sockets and the event stream.files.rs:GET /cms/api/raw/{*path}serves a source file's bytes.build.rs:GET /cms/api/exportreturns the content as a zip. The batch, preview, raw and export routes are marked provisional in the route table.
- Test routes (
test_routes.rs,/test/…) exist only with thetest-routesfeature, which the e2e builds turn on. Without it they answer like any unknown path. public.rs: lichen's own files moved to/lichen-static/, so a site's/static/is its own.
Files (crates/lichen-site/src/)
api.rs— the route table, its groups and layers. Read this first.api/api_utils.rs— the error shape,respond(), the format and path helpers.api/files.rs(942 lines; much of it lichen-cms'scms.rs) — the tree, a file, edits, folders, renames, raw bytes.api/build.rs,api/site.rs— build, render, preview, check, export; info and settings.api/history.rs,api/batch.rs,api/live.rs,api/preview.rs— the new topics.api/filetree.rs(from lichen-cms'sfiletree.rs),api/public.rs,api/test_routes.rs.crates/lichen-cms/src/cms.rs(−982),crates/lichen-cms/src/router.rs(−362) — deleted; their code is in the files above and in #34.
Lives elsewhere
- Page URLs, the escape hatch and the frontend and plugin routes (
api/html.rs,api/frontend.rs,api/plugins.rs): #34. - The room machinery behind
live.rs: #29. The preview build behindpreview.rs, and the rebuild lane: #27. The no-JavaScript save form: #26. - The site's policy and
Permissions: #18. The front server's own site routes (front_site_routesinserver.rs): #30, which forwards the publish (run by the admin thread, #25) and the git editor routes (#23). - Not in any PR (base commit): the API contract
contract/cms-api.json(hand-written; the server's tests compare the routes with it),api/yjs-demo.html, lichen-ts's client for these routes, and the tests.
Category: The site's API: files, batch, history, live and preview routes
Size: 14 files, +4,345/−1,415
Rounds: round 0 at stack ae42e3c0; round 1 at 6b650b2a (SECURITY-FOLDER-AND-PRINCIPALS): api/files.rs adds read_refused_bodies, which reads each body through a channel and drains what its handler leaves, up to a limit, so a refusal given before the body is read reaches the front server as a 413 (SFP fix-up 4b, 128873c6; layered by #19). Read that round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/35/round/1/interdiff; round 2 at b66397c0 (FULL-SITE-PROCESS): api.rs names the push permissions as rights to ask (TriggerPushToRemote, TriggerPushToPds, 3d, the plan's O4), and says a publish is the admin thread's (6e). Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/35/round/2/interdiff
Plans and ledger: FULL-SITE-PROCESS-2026-10-25.md (3d, 6e); OPERATOR-REVIEW-FOLLOWUPS-2026-09-30.md (item 6, steps 0–4, with items 9 and 11; checks archived in machine-docs/archive/api-layer-migration-2026-10/), OPERATOR-REVIEW-FIXES-2026-10-07.md (U1), FRONTEND-API-AND-LOADER-2026-09-24.md (the /cms/api the loader's frontends call), LICHEN-SANDBOX-AND-PERMISSIONS-2026-10-13.md (L2d), SITE-FRONT-BOUNDARY-2026-10-17.md (9a, 11c).
Inline #[cfg(test)] modules: api/filetree.rs (preview_map_tests, hugo feature). Also 4 files point to a test file with #[path] (api.rs, api/batch.rs, api/history.rs, api/preview.rs; api/filetree.rs has one too), and api/files.rs has test-only items.
Bucket moves: cms.rs and router.rs (deleted, not in round 1) are placed here by an explicit rule, because git pairs them with api/files.rs and api/public.rs at 45% and 35%, below its 50% rename threshold. api/frontend.rs, api/plugins.rs and api/html.rs went to #34.
Round 1: #13 (Site crate core) covered api.rs, api/api_utils.rs, api/batch.rs, api/build.rs, api/files.rs and api/site.rs. The other files are new to the set in this range.