diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index c49e695..cbd8d17 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -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 diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index a31a126..bcfc2ea 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -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. diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 35f1fa6..3a5a316 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -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: , errors: [] }`, 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