docs(website): demo mode step 2 built — DM18, MODULE_API 1.19.0 #380

Merged
whitlocktech merged 1 commits from docs/demo-mode-module into main 2026-10-26 05:51:15 -05:00
3 changed files with 53 additions and 8 deletions

View File

@@ -1828,6 +1828,10 @@ it.
`teams_voice_*`, `demo_*`).
A condition that cannot decide refuses.
- **Event steps** (`MODULE_API` 1.19.0, DM18). `POST /admin/events`, `PUT /admin/events/:id` and
`POST /admin/events/:id/publish` ask each step's action `demoRefuses(params)`, when it declares one.
A sentence back refuses the write `403 { code: "DEMO_LOCKED", message, errors }` with that sentence; a
hook that throws refuses too.
- **Uploads.** Any request with a `multipart/*` content type is refused app-wide, before a router or
multer sees it.
- **Search engines.** Every response carries `X-Robots-Tag: noindex, nofollow`. `/robots.txt` is

View File

@@ -1,7 +1,8 @@
# Demo mode — `demo.runicgateway.com`
**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.
**Status:** design **fully decided** 2026-10-23 (DM1-DM18, §9). §10 step 1 (core, `MODULE_API`
1.18.0) is **built**, and so is step 2 (the module, with its core hook `MODULE_API` 1.19.0, DM18);
steps 3-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.
@@ -177,9 +178,13 @@ for.
denied command named;
- when the step **runs**, so an event saved before the list changed is still refused.
The module learns that it runs on the demo from a new read-only `ctx.demoMode` boolean. That is part
of the same `MODULE_API` minor bump as `demoLocks` (§3.2). The list lives in the module with tests,
and a command added later is a module change.
Saving an event is core's route, and core never asked a module about a step's params when it saved.
So the save-time check is a core hook (DM18, `MODULE_API` **1.19.0**): an event action may declare
`demoRefuses(params)`, which returns a sentence or `null`. With `DEMO_MODE` on, core asks it for every
step when a definition is created, saved or published, and the first sentence refuses the write
`403 DEMO_LOCKED` with that sentence as the message. A hook that throws refuses too. The run-time check
reads a new read-only `ctx.demoMode` boolean, part of the same `MODULE_API` minor bump as `demoLocks`
(§3.2, 1.18.0). The list lives in the module with tests, and a command added later is a module change.
Commands that only change the world or the players (`server.writecfg`, `entity.*`, `env.*`,
`weather.*`, `say`, `kick`, `ban`, `oxide.grant`) stay open. The baseline restore puts all of them
@@ -324,6 +329,8 @@ org lead created the subuser and added the secret on 2026-10-23.
UTC, and the published logins.
The fixture is JSON: `{ accounts, settings?, content?: { posts, wiki, pages, events }, modules? }`.
The Rust module's section is `{ servers: [{ id, name, sidecarBaseUrl, sidecarToken, protocol?,
enabled?, sortOrder? }], links: [{ username, steamId, name?, serverId? }] }`.
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
@@ -414,6 +421,7 @@ down.
| 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) |
| DM18 | (build, step 2) Saving an event is core's route, and core never asks a module about a step's params. How is a denied command refused at save? | **A new core hook:** an event action's optional `demoRefuses(params)`, asked at create, save and publish on the demo, refusing `403 DEMO_LOCKED` (§3.4). `MODULE_API` 1.19.0; Module-Rust then needs `^1.19.0` |
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
@@ -442,7 +450,15 @@ through that proxy.
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.
manifest (§7.5). Each with tests. One PR. The save-time half of the deny list needed a core hook
first (DM18): `demoRefuses` in `website`, `MODULE_API` 1.19.0, its own PR ahead of the module's.
**Built 2026-10-26:** core `website#224`, module `Module-Rust#62` (`coreApi` `^1.19.0`). The
module's rules are in `server/demo.js`, and its seed hook is `server/demoSeed.js`, which reads
`modules.rust.servers` and `modules.rust.links`. A bridge path is recognised after
canonicalising it (dropping `.` segments, trimming, lower-casing), so `./RunicGateway.json`
counts. The plugin name `RunicGateway` is a floor beside the catalogue's `self`, so a catalogue
that cannot be read never unlocks the file. A config that does not parse is withheld whole on the
demo, because its secrets cannot be found to redact.
3. **Kit** (`Integration-kit`): a paragraph on `demoLocks` and `ctx.demoMode`, and the
`core-ref.json` re-pin. One PR.
4. **App** (`Android-app`): the `DEMO_LOCKED` error kind and the banner on Home. One PR.

View File

@@ -26,13 +26,27 @@ here extends the contract first, in this file, before the module is written agai
Core exports a single integer-major semver string from `server/src/modules/version.js`:
```js
const MODULE_API_VERSION = '1.18.0'
const MODULE_API_VERSION = '1.19.0'
```
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
module chunk evaluates, which is earlier than any network round trip could answer.
**1.19.0 — the public demo refuses a step at save** (`website/DEMO_MODE.md` §3.4, DM18). One optional
member on an event action, so minor.
- **`demoRefuses(params)`** (§2.4, `registerEventActions`): a function of a step's checked params that
returns a sentence, or `null` to let the step through. Asked only with `DEMO_MODE` on, for every step
when a definition is created, saved or published. The first sentence refuses the whole write
`403 { code: 'DEMO_LOCKED', message: <the sentence>, errors: [<the sentence>] }`, so core's one
refusal toast shows it. A hook that throws refuses too, with core's own sentence, because a lock that
cannot decide fails closed.
- The hook covers the **save**. The module still refuses in `perform()` (behind `ctx.demoMode`),
because a step can be saved before the module's list grows.
- A module that declares it needs `coreApi` `^1.19.0`: an older core rejects nothing but never calls
it, so the save would go through. Module-uo's `^1.10.0` still resolves and declares none.
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3; website#222). Three optional
additions and no removal, so minor. With `DEMO_MODE` unset, none of them changes anything a module sees,
except that `demoLocks` is validated.
@@ -538,6 +552,10 @@ branches lacks — so it is the only place `permits` is true between two values
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
unchanged, and so is every rule about composition narrowing rather than widening.
**1.19.0 — a step refused at save on the demo** (`website/DEMO_MODE.md` §3.4, DM18). An event action's
optional `demoRefuses(params)`, asked at create, save and publish with `DEMO_MODE` on; one optional
member, so minor; see §1.1. Module-uo's `^1.10.0` still resolves.
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3). `demoLocks` in `module.json`,
`ctx.demoMode` and `api.onDemoSeed`; three optional additions, so minor; see §1.1. Module-uo's
`^1.10.0` still resolves.
@@ -981,7 +999,7 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) //
api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0
api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0
api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0; a dry run's detail.warnings 1.16.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress, demoRefuses }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0; a dry run's detail.warnings 1.16.0; demoRefuses 1.19.0
api.registerEventBudgets([{ id, label, unit }]) // 1.10.0
api.registerEventLeases([{ id, label, type, min, max, values, example, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0; example 1.13.0
api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0; a row's note 1.13.0
@@ -1396,6 +1414,13 @@ rather than implementation and belong here:
would hide what the line describes answers `null` for that reader; a public page must never say
what the module's own pages would hide. Core gives it 2 s and drops a late answer. It is a read: it
must change nothing, and it may be called every few seconds for as long as a run is live.
- **`demoRefuses(params)` is the demo's save-time lock, and it is not the only one** (1.19.0). Asked
only with `DEMO_MODE` on, at create, save and publish, with the step's checked params; a sentence
refuses the write `403 DEMO_LOCKED` and is shown as written, so phrase it for the person saving
(`"quit" is turned off on the demo site.`). It is a pure check: no database, no game, nothing changed.
A throw refuses. Because a definition can predate the list it is checked against, the module checks
again in `perform()` behind `ctx.demoMode`. It never leaves the process: the catalog strips it with
the other callables.
- **A template is a shortcut past the editor, never a second way to make an event** (1.13.0). What
`build()` returns is validated as a spec and opened as an unsaved draft; the author saves it
through the same route, with the same checks, as one they typed. So a template cannot make a