lichen.page.review from the reviewed point 0b92cb61 to stack eb5a1653, as one PR per subsystem (review set)

full 18: The site's frontends and plugins: choosing, loading and serving them #81

targeting full/17-dashboard-server from full/18-site-frontends
Open opened by and assigned to notplants-bot.bsky.social
targeting full/17-dashboard-server from full/18-site-frontends

How a site chooses, loads and serves the frontend its editor pages run. A site's [frontend] source is the built-in React editor, a local path: folder, an https: archive, or a git source (git+https:, tangled:); a remote one is pinned and kept as a checked copy under .lichen/. The same loader fetches the built-in editor's plugins (phase 13), and the front server reuses it for a host-selected dashboard. All of this is new since 09-23.


AST-verified changes

  • Item 6's api-layer move (7601a57c) created api/html.rs and api/frontend.rs, moving the page and frontend handlers out of lichen-cms's cms.rs and router.rs. Those two files are deleted in #35, which holds the rest of the move. Checked by the builder: "3035 items before and after, 146 moved, 25 visibility changes as mapped, 3690 comments compared, equal". verify.sh run FUHHLT: "VERIFY OK (ast 3035 items/146 moved/25 visibility/3690 comments; routes 130=130 ...)". Later VERIFY OK runs: TRNKZ7, S1xZAJ, FZ0qAX, 1wrz6l, 0I9ss7. The adversary's run eMUcJc was VERIFY OK. Allowed: path rewrites (crate::cms/router → crate::api), member visibility, dropped file attributes, lone trailing commas.
  • OPERATOR-REVIEW-FIXES U1 (1d6fe2d4) moved shared helpers out of api/html.rs into api/api_utils.rs and its neighbours (#35). One check over 8b30050a..f6a10c32: "3004 items, 48 moved, 69 carried, 19 renamed, 2 visibility as mapped, 233 module declarations" (run id not recorded). The adversary's rerun, notify-abab368a, got the same counts.

Roughly 69 of this PR's 3,077 changed lines are code these moves carried verbatim. Every file here was created after 09-23 and changed a good deal after the moves, so in this diff they read as new code.


FRONTEND-API-AND-LOADER made the editor a replaceable frontend over the site's /cms/api, and phase 13 added plugins to the built-in:

  • Choosing a frontend (frontend.rs). [frontend] source is builtin (the default), path:<folder> (read live, through the site's confined tree), an https: archive, or git+https:/tangled:. Anything that cannot be served serves the built-in with a message saying why: no manifest, another API version, an unpinned or unfetchable source.
  • Archives (frontend/remote.rs). Fetched through the SSRF guard and pinned by sha256: over the unpacked files. The copy is kept in .lichen/frontends/<name>@<pin>/, outside git, and checked against its pin once per settings generation. A missing copy is fetched again in the background and kept only if it still matches. Limits: 20 MiB, no symlinks, no path that leaves the frontend, redirects re-checked. The loader has kinds, so the front server's dashboard source (DASHBOARD) and plugins (PLUGIN) use it too.
  • Git sources (frontend/git.rs). Pinned by commit and located through the guarded client. git fetches one commit (--depth 1) in a scratch folder, outside the site's lock, with hooks and submodules off, bounded in time. The files are read object by object, never checked out, with tree and size caps checked before any blob is read. The module's own docs state the DNS-rebinding window the address check leaves.
  • Plugins (plugins.rs, plugins/remote.rs). A site's [[plugins]] list, after lichen.toml's defaults in multi-site mode (inherit_plugins = false drops them). Built-in plugins ship in the binary; remote ones are pinned and fetched like frontends into .lichen/plugins/. One plugin that cannot be served is refused alone.
  • Page URLs (api/html.rs). Every /cms/ page path (item 9) serves the selected frontend. ?frontend=builtin is the escape hatch to the built-in, and ?format=html gives the no-JavaScript editor, now the built-in drawn by React on the server (REPLACE-TERA R1). While settings.toml cannot be read, pages redirect to its own page (repair mode).
  • Routes: POST /cms/api/frontend/update pins a remote frontend (api/frontend.rs). GET /cms/api/plugins, a plugin's files, and /cms/api/plugins/{name}/update are in api/plugins.rs.
  • build.rs — new: release builds watch the embedded folders (the built-in app, its plugins, lichen-ts/dist, the no-JavaScript static files), so a changed bundle rebuilds the binary.

Files (crates/lichen-site/)

  • src/frontend.rs — choosing what a page serves, and why not. Read this first.
  • src/frontend/remote.rs (838 lines) — archives, pins, copies, and the loader's kinds.
  • src/frontend/git.rs (590 lines) — git sources.
  • src/plugins.rs, src/plugins/remote.rs — plugin lists and remote plugins.
  • src/api/html.rs — page URLs, the escape hatch, the server-drawn editor, repair mode.
  • src/api/frontend.rs, src/api/plugins.rs — their API routes.
  • build.rs — the embedded folders' rebuild watch.

Moved in: all nine files, by an explicit rule. Round 1 put frontend/*, api/html.rs and build.rs in the dashboard PR, because the dashboard source reuses the loader, and api/frontend.rs and api/plugins.rs in site core. Here they are one subject: how a site serves a frontend.

Lives elsewhere

  • The rest of the site's API, and the deletions of cms.rs and router.rs: #35. The route table and its permission groups (api.rs): #35.
  • The SSRF guard (ssrf_guard.rs) and the settings the loader reads: #37. The front server's dashboard source, which uses this loader: #33.
  • The server render the no-JavaScript editor uses: #26.
  • Not in any PR (base commit): the built-in editor and its plugins (frontends/builtin/), frontends/no-javascript/, lichen-ts, the built bundles and the tests (frontend/tests/*, plugins/tests/*, tests/plugins/).

Category: The site's frontends and plugins: choosing, loading and serving them Size: 9 files, +3,077/−0 Rounds: round 0 at stack ae42e3c0. At 29a7c15b, 6b650b2a and b66397c0 none of this PR's files changed, so round 0 is current; the branch was merged up so that the stack's top equals b66397c0. Plans and ledger: FRONTEND-API-AND-LOADER-2026-09-24.md (the loader; pieces s/23 archives, s/24 git, s/25 update), PHASE-13-UNIT-1A-DESIGN-2026-10-04.md, -UNIT-2-, -UNIT-4- (plugins), OPERATOR-REVIEW-FOLLOWUPS-2026-09-30.md (items 6 and 9), REPLACE-TERA-WITH-SSR-REACT-2026-10-07.md (R1: the server-drawn no-JavaScript editor), DASHBOARD-REACT-2026-10-07.md (the loader's dashboard kind). Inline #[cfg(test)] modules: none. 5 files point to a test file with #[path] (frontend.rs, frontend/git.rs, frontend/remote.rs, plugins.rs, plugins/remote.rs). Bucket moves: all nine files are placed by an explicit rule (see Moved in). frontend.rs, plugins.rs and plugins/remote.rs were not in round 1, which had no change to them. Round 1: #12 (Dashboard) covered frontend/git.rs, frontend/remote.rs, api/html.rs and build.rs; #13 (Site crate core) covered api/frontend.rs and api/plugins.rs.

Commits 0