feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0) #223

Merged
whitlocktech merged 1 commits from feature/demo-mode-core into main 2026-10-23 19:05:10 -05:00
Member

Core half of the public demo (#222). This is step 1 of docs/website/DEMO_MODE.md §10. Design PR: docs#377 (merged). Docs for this PR: RunicGateway/docs PR on docs/demo-mode-core.

Nothing here runs unless DEMO_MODE=true. app.js mounts none of it otherwise, and a test proves it (demoAppOff.test.js).

Server

  • The lock (server/src/demo/). It is mounted at /api/v1, ahead of every router. A match answers 403 { message, code: "DEMO_LOCKED" } before the route's own auth check. Reads are never locked.

    • It covers every Tier A row of §3.3.
    • Admin writes to /admin/users/:id… are refused when the target is a demo account.
    • /auth/me/… account changes are refused when the caller is signed in as one. The account row decides, not the token's username claim.
    • DM16: POST /auth/register is locked.
    • DM17: DELETE /auth/me/sessions/:id is locked on a demo account.
    • Matching follows express 4: case-insensitive, and the trailing slash optional. A condition that cannot decide (database down) refuses.
  • PUT /admin/settings keys. That route writes arbitrary keys, so {site_mode: "maintenance"} there got around the /admin/site-mode lock. On the demo it now refuses a body naming site_mode*, module_source_hosts, teams_voice_* or demo_*, and names the keys. Theme, nav and the other settings stay open (DM2). This applies locks you already decided; it adds no new decision. ⚠️ Flagging it for review anyway.

  • Uploads. Any multipart/* request is refused app-wide. Plus X-Robots-Tag: noindex and robots.txt: Disallow: /.

  • Loader (MODULE_API 1.18.0, both halves).

    • demoLocks in module.json: writes only, under the module's own mounts, validated on every boot.
    • ctx.demoMode.
    • api.onDemoSeed (DM15).
  • GET /public/settings gains demo: { resetsAt, logins } on the demo only.

  • npm run seed:demo -- <fixture>:

    • upserts the four accounts;
    • sets the site live with registration disabled;
    • seeds content on the first run only;
    • calls each module's onDemoSeed;
    • stamps demo_state.

    It resolves "$env:NAME" strings, so the rig token never sits in the repo. It refuses to run off the demo.

Client

  • The demo banner, from settings.demo.
  • One DEMO_LOCKED toast, fed from the request primitive, so installed modules' screens get it too.
  • The published logins on /account/login and /admin/login, with a Use button.
  • Dashboard (site mode) and Register said "only an administrator…" for any 403. They now say the demo message.

Verified

  • npm test (server): 2206 pass, 0 fail, with MODULES_DIR pointed away from a local modules/ checkout. That checkout makes routeManifest.test.js fail on clean main too.
  • npm test (client): 435 pass. npm run build is OK.
  • New tests:
    • demoLocks.test.js: every core row is held against routes.manifest.json; every upload route is locked by name.
    • demoModule.test.js: loader, ctx.demoMode, onDemoSeed, the public block, and the seed helpers.
    • demoApp.test.js / demoAppOff.test.js: the real app with the flag on and off.
  • Live run on a scratch DB (DEMO_MODE=true, a probe module declaring a demoLock and an onDemoSeed):
    • seed:demo ran twice. The re-run left content alone and reset the accounts.
    • Signed in as demo-admin, 20 HTTP probes all answered as designed.
    • In Chrome: the banner, the login list with Use → sign in, and the toast plus inline message on "Switch to Maintenance".
  • Swagger regenerated. routes.manifest.json has a zero-line diff. engagement-triggers.json was regenerated (it embeds the API version).

Not in this PR

  • Module-Rust (step 2), the kit re-pin (3), Android (4), and deploy/demo/ with the workflows (5).

  • AI-assisted: written with Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Core half of the public demo (#222). This is step 1 of `docs/website/DEMO_MODE.md` §10. Design PR: docs#377 (merged). Docs for this PR: RunicGateway/docs PR on `docs/demo-mode-core`. **Nothing here runs unless `DEMO_MODE=true`.** `app.js` mounts none of it otherwise, and a test proves it (`demoAppOff.test.js`). ## Server - **The lock** (`server/src/demo/`). It is mounted at `/api/v1`, ahead of every router. A match answers `403 { message, code: "DEMO_LOCKED" }` before the route's own auth check. Reads are never locked. - It covers every Tier A row of §3.3. - Admin writes to `/admin/users/:id…` are refused when the target is a demo account. - `/auth/me/…` account changes are refused when the caller is signed in as one. The account row decides, not the token's username claim. - **DM16:** `POST /auth/register` is locked. - **DM17:** `DELETE /auth/me/sessions/:id` is locked on a demo account. - Matching follows express 4: case-insensitive, and the trailing slash optional. A condition that cannot decide (database down) refuses. - **`PUT /admin/settings` keys.** That route writes arbitrary keys, so `{site_mode: "maintenance"}` there got around the `/admin/site-mode` lock. On the demo it now refuses a body naming `site_mode*`, `module_source_hosts`, `teams_voice_*` or `demo_*`, and names the keys. Theme, nav and the other settings stay open (DM2). This applies locks you already decided; it adds no new decision. ⚠️ **Flagging it for review anyway.** - **Uploads.** Any `multipart/*` request is refused app-wide. Plus `X-Robots-Tag: noindex` and `robots.txt: Disallow: /`. - **Loader (MODULE_API 1.18.0, both halves).** - `demoLocks` in `module.json`: writes only, under the module's own mounts, validated on every boot. - `ctx.demoMode`. - `api.onDemoSeed` (**DM15**). - **`GET /public/settings`** gains `demo: { resetsAt, logins }` on the demo only. - **`npm run seed:demo -- <fixture>`**: - upserts the four accounts; - sets the site live with registration disabled; - seeds content on the first run only; - calls each module's `onDemoSeed`; - stamps `demo_state`. It resolves `"$env:NAME"` strings, so the rig token never sits in the repo. It refuses to run off the demo. ## Client - The demo banner, from `settings.demo`. - One `DEMO_LOCKED` toast, fed from the request primitive, so installed modules' screens get it too. - The published logins on `/account/login` and `/admin/login`, with a **Use** button. - Dashboard (site mode) and Register said "only an administrator…" for any 403. They now say the demo message. ## Verified - `npm test` (server): **2206 pass, 0 fail**, with `MODULES_DIR` pointed away from a local `modules/` checkout. That checkout makes `routeManifest.test.js` fail on clean `main` too. - `npm test` (client): **435 pass**. `npm run build` is OK. - New tests: - `demoLocks.test.js`: every core row is held against `routes.manifest.json`; every upload route is locked by name. - `demoModule.test.js`: loader, `ctx.demoMode`, `onDemoSeed`, the public block, and the seed helpers. - `demoApp.test.js` / `demoAppOff.test.js`: the real app with the flag on and off. - **Live run** on a scratch DB (`DEMO_MODE=true`, a probe module declaring a `demoLock` and an `onDemoSeed`): - `seed:demo` ran twice. The re-run left content alone and reset the accounts. - Signed in as demo-admin, 20 HTTP probes all answered as designed. - In Chrome: the banner, the login list with **Use** → sign in, and the toast plus inline message on "Switch to Maintenance". - Swagger regenerated. `routes.manifest.json` has a zero-line diff. `engagement-triggers.json` was regenerated (it embeds the API version). ## Not in this PR - Module-Rust (step 2), the kit re-pin (3), Android (4), and `deploy/demo/` with the workflows (5). - [x] AI-assisted: written with Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-10-23 18:52:44 -05:00
feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 43s
PR Checks / client-build (pull_request) Successful in 1m3s
PR Checks / server-tests (pull_request) Successful in 8m37s
4ad746c43e
Core half of the public demo (docs/website/DEMO_MODE.md §10 step 1, website#222).
Nothing here runs unless DEMO_MODE=true.

- server/src/demo/: the lock at /api/v1, ahead of every router. It covers every
  Tier A row of §3.3, the shared-login conditions, POST /auth/register (DM16),
  DELETE /auth/me/sessions/:id on a demo account (DM17), and the PUT
  /admin/settings keys that reach a Tier A lock. Any multipart request is refused
  app-wide. noindex and robots.txt. A condition that cannot decide refuses.
- Loader: demoLocks in module.json (writes only, under the module's own mounts),
  ctx.demoMode, and api.onDemoSeed (DM15). MODULE_API 1.18.0 on both halves.
- GET /public/settings carries demo: { resetsAt, logins } on the demo only.
- npm run seed:demo -- <fixture>: upserts the four accounts, sets the site live
  with registration disabled, seeds content on the first run only, calls each
  module's onDemoSeed, and stamps demo_state. It resolves $env: strings and
  refuses to run off the demo.
- Client: the banner, one DEMO_LOCKED toast fed from the request primitive (so
  module screens get it too), and the published logins on both sign-in pages.
  Dashboard and register say the demo message instead of blaming a role.
- OpenAPI: DemoLocked schema, the demo block, and a note that any write may
  answer DEMO_LOCKED on the demo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech approved these changes 2026-10-23 18:58:27 -05:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-10-23 18:58:43 -05:00
whitlocktech merged commit c6f53a1509 into main 2026-10-23 19:05:10 -05:00
whitlocktech deleted branch feature/demo-mode-core 2026-10-23 19:05:11 -05:00
Sign in to join this conversation.
No description provided.