cms
The cms block is the whole authenticated zone. Its keys:
| cms key | description |
|---|---|
| path | only "/cms" (reserved) |
| auth | sessions and sign-in — see auth |
| menu | the admin's own router (below); the item with path "/" is the admin index (required, exactly one) |
| guard | OPTIONAL — hide /cms behind a device cookie (see below). Omit it and /cms is open as usual |
The admin skeleton is a partial the admin templates wrap themselves in — this site declares it as _cms. A menu item binds a path to an hbs template with data views, exactly like a public route binds a site page.
| menu item key | description |
|---|---|
| path | "/" for the index, "/services" for a section (one segment); the page opens at /cms + path |
| title | label in the nav and on the index tiles |
| hbs | template of the section page |
| view / views | data, like on page routes; the built-in users and media views serve the system tables |
| form | the edit binding: { "name": "doc_edit", "hbs": "cms/form.hbs" } — which table form edits the section rows and which template renders the row page (/cms/{path}/{id}). No form — a read-only section |
| permissions | roles allowed in (admin always is; empty — admin only). The users section is admin-only no matter what — the engine ignores wider permissions there |
Two sections of this site:
"cms": {
"path": "/cms",
"auth": { "session_ttl_minutes": 120, "cookie_name": "cms_session" },
"menu": [
{ "path": "/", "title": "Dashboard", "hbs": "cms/index.hbs", "permissions": ["user"] },
{ "path": "/docs", "title": "Articles", "hbs": "cms/list.hbs",
"view": "doc_admin", "permissions": ["user"],
"form": { "name": "doc_edit", "hbs": "cms/form.hbs" } }
]
// , "guard": [] // optional: enable the /cms gate (see below)
}(This example site ships without guard — the line is shown commented for placement; it sits right beside menu.)
The nav is built by the layout from the menu itself — {{#each menu}}, already filtered by the visitor's role: an admin-only section simply does not exist for the user role, in the nav or by direct URL. Every admin template is an ordinary site-owned hbs file; only the sign-in pages (cms/login, cms/invite) keep fixed names — they run the OPAQUE flow before any session exists. Details: Admin templates & the WASM client.
Hiding the admin (optional). guard is a key of the cms block (a sibling of path/auth/menu, not a menu item) that makes the whole /cms zone invisible to browsers without an enrolled device: with the gate on, /cms and everything under it answer the same 404 as any unknown path — the login page is not merely locked, it does not appear to exist. This is a curtain, not a replacement for the password: real OPAQUE sign-in still runs behind it. A fresh bootstrap (plain ./cms in an empty folder) ships with the gate ON and prints the one-time enrollment link in the console first-run summary. Three states:
| cms.guard | meaning |
|---|---|
| key absent | no gate — /cms is open as usual (this example after def-help) |
[] | gate ON, zero devices — /cms is hidden from EVERYONE until you add one (issue a device link first, or you lock yourself out) |
| list of devices | gate ON — only enrolled devices see /cms. Managed by the server, do not hand-edit |
The device list is managed from the CLI, and the cookie key is kept in settings.json as a sha3 hash (a different vault from the password keys in the database):
cms guard add laptop --ttl 24h # prints a one-time link like /Qx7f… ; open it once from that device cms guard list # active / pending / expired devices cms guard remove laptop # revoke a device
Opening the link sets a long-lived cms_guard cookie and burns the link. Every guard command takes --site domain to manage a named site of the map; without it the default site is managed. Inviting a new user while the gate is on: send them a guard link first, then the invite link.