docs(website): demo mode core built — DM15-DM17, MODULE_API 1.18.0
- DEMO_MODE.md: DM15 (onDemoSeed), DM16 (POST /auth/register locked), DM17 (sessions route locked outright on a demo account). It also covers the settings keys that reach a Tier A lock, how demo accounts are recognised, failing closed, the published logins, the toast, the seed's fixture shape and $env, and the fact that there is no seeded team (Teams come from the rig's clans). The §3.2 example drops the config-file POST that DM14 opened. Step 1 is marked built. - MODULE_API.md: 1.18.0. demoLocks in module.json, ctx.demoMode, and api.onDemoSeed, which must upsert because it runs on every seed. - BACKEND_DESIGN.md: §5.1 demo mode, and demo_state in the settings table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Demo mode — `demo.runicgateway.com`
|
||||
|
||||
**Status:** design, **fully decided** 2026-10-23 (DM1-DM14, §9). Ready to build per §10. Nothing is
|
||||
built yet.
|
||||
**Status:** design **fully decided** 2026-10-23 (DM1-DM17, §9). §10 step 1 (core, `MODULE_API`
|
||||
1.18.0) is **built**; steps 2-6 are not.
|
||||
**Issue:** `RunicGateway/website#222`. **Why now:** Google Play (`../android/PLAN.md` item 9,
|
||||
M8) needs a public HTTPS site with working sign-ins for its reviewers and for the 12 testers who
|
||||
must be opted in for 14 days before the app can apply for production.
|
||||
@@ -67,16 +67,19 @@ routes in `module.json` (DM1, §3.2).
|
||||
```json
|
||||
"demoLocks": [
|
||||
{ "method": "PUT", "path": "/admin/rust/servers/:id" },
|
||||
{ "method": "DELETE", "path": "/admin/rust/servers/:id" },
|
||||
{ "method": "POST", "path": "/admin/rust/config/:serverId/file" }
|
||||
{ "method": "DELETE", "path": "/admin/rust/servers/:id" }
|
||||
]
|
||||
```
|
||||
|
||||
Paths use the same `:param` syntax as the module's routers and must sit under the module's own
|
||||
mounts, which the loader checks the same way it checks `mounts`. A module without the field is
|
||||
unaffected. It is a new manifest field, so `MODULE_API_VERSION` gets a **minor** bump,
|
||||
`MODULE_API.md` documents it, and the Integration Kit's module chapter gains a paragraph (its
|
||||
`ci/core-ref.json` pin then moves, as for any API bump).
|
||||
Paths use the same `:param` syntax as the module's routers, with an optional trailing `/*` for a
|
||||
path and everything under it. They must sit under the module's own mounts, which the loader checks
|
||||
the same way it checks `mounts`. Only writes can be declared. The config editor's one locked read
|
||||
(§3.5) depends on which file is asked for, so the module enforces it behind `ctx.demoMode` rather
|
||||
than by route. A module without the field is unaffected. The field is validated on every boot, so a
|
||||
wrong declaration fails on a developer's machine and not only on the demo. It is a new manifest field,
|
||||
so `MODULE_API_VERSION` gets a **minor** bump to **1.18.0**, `MODULE_API.md` documents it, and the
|
||||
Integration Kit's module chapter gains a paragraph (its `ci/core-ref.json` pin then moves, as for any
|
||||
API bump).
|
||||
|
||||
### 3.3 What is locked
|
||||
|
||||
@@ -96,8 +99,14 @@ wipe puts them back (§7).
|
||||
| Team voice | writes under `/admin/teams/voice` | Configure or sync Discord voice channels |
|
||||
| Demo accounts, by an admin | `PUT`/`DELETE /admin/users/:id`, `DELETE …/trusted-devices…`, `POST …/mfa/reset` **when `:id` is a demo account** | Demote, disable or delete the account everyone signs in with |
|
||||
| Own account, on a demo account | `PATCH /auth/me/account/username`, `…/password`, `…/email`; `POST /auth/me/account/totp/setup`, `…/enable`, `…/disable`; `POST /auth/me/account/recovery-codes/generate`; `DELETE /auth/me/account/identities/:provider` | Change the shared password, or turn on 2FA so nobody else can sign in |
|
||||
| Other people's sessions | `DELETE /auth/me/sessions/:id` for a session that is not the caller's | Sign every other visitor out of the shared account |
|
||||
| Other people's sessions | `DELETE /auth/me/sessions/:id` **on a demo account**, whichever session it names (DM17) | Sign every other visitor out of the shared account |
|
||||
| Password reset | `POST /auth/password/forgot`, `POST /auth/password/reset/:token` | Reset a demo account's password |
|
||||
| Registration | `POST /auth/register` (DM16) | Settings are open, so a visitor could switch `player_registration` on. The route is what keeps registration closed (§4) |
|
||||
| Settings that reach a lock | `PUT /admin/settings` when the body names `site_mode*`, `module_source_hosts`, `teams_voice_*` or `demo_*` | That route writes arbitrary keys, so these would get around the site-mode, Modules and team-voice rows above, or rewrite the banner and the published logins. The answer names the keys (`keys: [...]`) |
|
||||
|
||||
**Why DM17 locks the whole route.** A mobile access token carries no id for its refresh-token row,
|
||||
so the server cannot tell which row in `GET /auth/me/sessions` is the caller's own. Signing out is
|
||||
`POST /auth/mobile/logout`, which revokes the caller's own row and stays open.
|
||||
|
||||
**Tier A — always locked (Rust module)**
|
||||
|
||||
@@ -107,7 +116,17 @@ wipe puts them back (§7).
|
||||
| The bridge's config, **read or written** | `GET`/`POST /admin/rust/config/:serverId/file`, `GET …/writes…` for the `RunicGateway` plugin's files, which are also left out of `GET …/files` | It holds the bridge's link to the sidecar and its credentials. Read, it would let someone talk to the sidecar directly over HTTP (DM14, §3.5) |
|
||||
|
||||
**Settings stay open (DM2).** Theme, navigation and the other `PUT /admin/settings` values can be
|
||||
changed and come back at the wipe. Brand images are uploads, so they fall under the upload lock.
|
||||
changed and come back at the wipe, except the keys in the "Settings that reach a lock" row above.
|
||||
Brand images are uploads, so they fall under the upload lock.
|
||||
|
||||
**Which accounts are demo accounts.** The four usernames in §4 are fixed in core
|
||||
(`server/src/demo/mode.js`), not read from the seed fixture: the lock checks them on every request,
|
||||
and a fixture that dropped one would unlock the account everybody shares. The lock decides by the
|
||||
account row in the database (`users.getById`), not by the username claim in the token. An account an
|
||||
admin creates on the demo (`POST /admin/users` stays open) is an ordinary account. It is gone at the
|
||||
next wipe.
|
||||
|
||||
**Failing closed.** A conditional row that cannot decide, because the database is down, refuses.
|
||||
|
||||
**Uploads — locked hard (DM4)**
|
||||
|
||||
@@ -211,7 +230,9 @@ All of this is enforced in the module's server code behind `ctx.demoMode` (DM10)
|
||||
| `play-review` | admin | Google's reviewer. Same powers as `demo-admin`, but a separate account, so a reviewer is never signed out because someone else revoked a session on the shared one |
|
||||
|
||||
Usernames and passwords are published on the demo's sign-in page and on runicgateway.com `/beta`.
|
||||
They are not secrets. The seed recreates them at every wipe, so a password that somehow changed is
|
||||
They are not secrets. The seed writes them into `demo_state`, `GET /public/settings` serves them as
|
||||
`demo.logins`, and both sign-in pages (`/account/login`, `/admin/login`) list them with a **Use**
|
||||
button that fills the form. A fixture account with `"published": false` is created but not listed. The seed recreates them at every wipe, so a password that somehow changed is
|
||||
back within one cycle. Public registration is **closed** on the demo: there is no mail to verify an
|
||||
address with (§5), and the static logins cover every role.
|
||||
|
||||
@@ -226,10 +247,16 @@ address with (§5), and the static logins cover every role.
|
||||
|
||||
## 6. The banner
|
||||
|
||||
`GET /api/v1/public/settings` gains `demo: { resetsAt }` while the flag is on. The web client shows a
|
||||
thin strip on every page: "Demo site — anything you change is wiped at 14:00 UTC. Sign-ins:
|
||||
demo-admin / demo-player …". The app shows the same strip on Home. Both read `resetsAt`, so neither
|
||||
needs to know the schedule.
|
||||
`GET /api/v1/public/settings` gains `demo: { resetsAt, logins }` while the flag is on, and only then.
|
||||
The seed writes both into one settings row, `demo_state`, which `PUT /admin/settings` cannot touch
|
||||
(§3.3). The web client shows a thin strip on every page: "Demo site — anything you change is wiped at
|
||||
14:00 UTC. Sign-ins: demo-admin / demo-player … — passwords on the sign-in page". The app shows the same
|
||||
strip on Home. Both read `resetsAt`, so neither needs to know the schedule. The time is said in UTC,
|
||||
not converted, so it matches the schedule published on `/beta`.
|
||||
|
||||
**The toast.** Every web request goes through one request primitive (`client/src/api/client.js`),
|
||||
installed modules' requests included. A `DEMO_LOCKED` answer is reported from there to one toast
|
||||
(`DemoToast`), and the caller still gets its error as usual. So no screen has to learn the code.
|
||||
|
||||
## 7. The wipe
|
||||
|
||||
@@ -284,13 +311,28 @@ org lead created the subuser and added the secret on 2026-10-23.
|
||||
2. `docker compose pull && docker compose up -d` — the app boots on the latest `main` image, creates
|
||||
the schema, and installs the Rust module from `MODULES=rust@<version>=<manifest>`, which the workflow writes (§7.5).
|
||||
3. Wait for the app's healthcheck.
|
||||
4. `docker compose exec app npm run seed:demo` — one core script that, from a fixture file in the
|
||||
deploy directory:
|
||||
- creates the four accounts (§4) and `demo-player`'s stand-in Steam link (DM6);
|
||||
- creates the Rust server row with the rig's sidecar URL and token taken from `.env`, so the
|
||||
link survives every wipe without being stored in the fixture;
|
||||
- writes the seeded site content: news posts, wiki pages, the demo events and a team;
|
||||
- stamps `resetsAt` for the banner.
|
||||
4. `docker compose exec app npm run seed:demo -- <fixture.json>` (or `DEMO_SEED_FILE`) — one core
|
||||
script (`server/db/seedDemo.js`) that, from a fixture file in the deploy directory:
|
||||
- creates the four accounts (§4), or resets them if they exist: password, role, active, 2FA off;
|
||||
- sets the site live with registration disabled, plus any `settings` the fixture adds;
|
||||
- writes the seeded site content: news posts, wiki pages, pages and the demo events. It does
|
||||
this only on the first run against a database, so a re-run never duplicates it;
|
||||
- hands each installed module its `modules.<id>` section through the module's `onDemoSeed` hook
|
||||
(DM15, `MODULE_API` 1.18.0). The Rust module writes its own server row, with the rig's sidecar URL
|
||||
and token, and `demo-player`'s stand-in Steam link (DM6). Core never writes a module's tables;
|
||||
- stamps `demo_state`: `resetsAt`, the next `DEMO_RESET_EVERY_HOURS` slot (default 2) on the hour
|
||||
UTC, and the published logins.
|
||||
|
||||
The fixture is JSON: `{ accounts, settings?, content?: { posts, wiki, pages, events }, modules? }`.
|
||||
Any string `"$env:NAME"` in it is replaced with that variable from the host's `.env`, so the
|
||||
sidecar token is never in the repo. An unset variable fails the run instead of seeding an empty
|
||||
credential. The script checks the fixture before writing anything. It must name exactly the four
|
||||
accounts, each with a role and a password of 8 or more characters. It refuses to run at all without
|
||||
`DEMO_MODE=true`. The demo's `.env` leaves `ADMIN_USERNAME` unset, so the boot seed creates no fifth
|
||||
admin.
|
||||
|
||||
**There is no seeded team.** Core's Teams are synced from the game (a Rust clan becomes a Team), so
|
||||
the demo's team comes from a clan in the rig's baseline (§7.2), not from the fixture.
|
||||
5. The module's sync connects when the rig finishes booting, and its first import adopts the
|
||||
baseline's permissions and NPCs (§7.1).
|
||||
6. Smoke check, after a bounded wait: `GET /api/v1/public/status` answers, and the Rust server
|
||||
@@ -369,6 +411,9 @@ down.
|
||||
| DM12 | (F2) What "latest" means across the Rust link | The latest **Rust bundle** for the rig's plugins and sidecar, the newest module release on the same protocol, and the website's `main` image; the wipe updates the rig when the bundle changes (§7.5) |
|
||||
| DM13 | (F3) DM2 read correctly? | Yes: only the settings stay open; every other Tier A row stays locked |
|
||||
| DM14 | (after DM13) May visitors change plugin configs? | Yes, every plugin except the bridge. The bridge's config can be **neither read nor written**, because it holds credentials that would let someone talk to the sidecar directly (§3.5) |
|
||||
| DM15 | (build) How does the seed create rows in a module's tables (the Rust server link, `demo-player`'s Steam link)? | Through a new optional module hook, **`api.onDemoSeed(data, { accounts })`**, in the same `MODULE_API` minor as `demoLocks`. The module writes its own rows with its own model; core never writes a module's tables (§7.3) |
|
||||
| DM16 | (build) Settings are open, so `player_registration` can be switched on. Lock registration? | Yes: **`POST /auth/register`** is a Tier A lock. Invites stay open (§3.3) |
|
||||
| DM17 | (build) The server cannot tell which mobile session is the caller's own. What does the other-sessions lock do? | **`DELETE /auth/me/sessions/:id` is locked outright on a demo account**; `POST /auth/mobile/logout` stays open (§3.3) |
|
||||
|
||||
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is
|
||||
setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
|
||||
@@ -389,7 +434,12 @@ through that proxy.
|
||||
demo-account and other-session checks), `demoLocks` loading, `ctx.demoMode`, the banner field,
|
||||
`seed:demo`, `noindex`, the web banner and toast, the `MODULE_API` minor bump, and the OpenAPI
|
||||
note that a route may answer `DEMO_LOCKED`. Tests for every lock. One PR, plus `MODULE_API.md`
|
||||
and this doc.
|
||||
and this doc. **Built 2026-10-23** as `MODULE_API` 1.18.0, with `onDemoSeed` (DM15), the
|
||||
registration lock (DM16), the protected settings keys (§3.3) and the published logins on the
|
||||
sign-in pages (§4). The code is in `server/src/demo/` (`mode.js`, `paths.js`, `locks.js`),
|
||||
`server/db/seedDemo.js`, and the client's `DemoBanner`, `DemoToast` and `DemoLogins`.
|
||||
`test/demoLocks.test.js` holds every core row against `routes.manifest.json`, so a row that stops
|
||||
naming a real route fails the build.
|
||||
2. **Module** (`Module-Rust`): `demoLocks` in `module.json`; behind `ctx.demoMode`, the console-command
|
||||
deny list (§3.4) and the config-editor rules (§3.5); and `protocol` in the release's install
|
||||
manifest (§7.5). Each with tests. One PR.
|
||||
|
||||
Reference in New Issue
Block a user