From c714906bbdf9697bd14340554ec1e32090490c40 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 17:43:52 -0500 Subject: [PATCH 1/4] docs(website): design demo mode for demo.runicgateway.com The design for website#222, before any code: one DEMO_MODE flag on the normal image that locks destructive writes with a 403 DEMO_LOCKED, four published static logins, no outbound mail/push/Discord, a banner with the next reset time, and a scheduled wipe on the demo-site runner (drop the volumes, redeploy, seed accounts + the rig link + content). Tier A locks are listed route by route. Nine questions for the org lead (module lock declaration, the rig, visitor content, rig sharing with walks, the stand-in Steam link, cadence, where the files live, version). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 1 + website/DEMO_MODE.md | 236 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 website/DEMO_MODE.md diff --git a/README.md b/README.md index ef3a827..5b5c88c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | +| [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, awaiting answers** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | | [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem | | [CLILOCS.md](website/CLILOCS.md) | UO's id → name table, so items have names. The shard decompresses and serves it over the bridge; the desktop conversion it replaced is gone | diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md new file mode 100644 index 0000000..9b2cb6f --- /dev/null +++ b/website/DEMO_MODE.md @@ -0,0 +1,236 @@ +# Demo mode — `demo.runicgateway.com` + +**Status:** design, waiting on the org lead's answers to §9. Nothing is built. +**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. + +## 1. What the demo is + +A public Runic Gateway deployment at `demo.runicgateway.com`, linked to the **Oxide Rust rig**, +that anyone can sign into with published accounts. The org lead set three requirements on +2026-10-23: + +1. **Static logins.** Ready-made accounts (player, moderator, admin, and one for Google's + reviewer) whose names and passwords are published. +2. **No destructive actions.** Someone signed in as the public admin must not be able to break the + site for the next visitor, or reach through it to damage the rig. +3. **An automatic wipe** every couple of hours that brings the site back to a known base: the Rust + module, the link to the rig, the demo accounts and the seeded content. + +The `demo-site` Gitea Actions runner is where the demo is deployed and wiped. The runner is only the +host for those jobs; the design below covers what runs there. + +## 2. Principle: the same image, switched by one flag + +The demo runs the normal `website-app` image and the normal Rust module release. Nothing is forked. +A single env var on the deployment, **`DEMO_MODE=true`**, turns on three things: + +- a **lock** on a fixed list of write routes (§3), answered before the route's own handler runs; +- a **banner** on every page and in the app saying this is a demo and when it next resets (§6); +- **no outbound side effects** that reach a real person (§5). + +With the flag unset, none of the demo code runs, so a normal shard is unaffected. + +## 3. The lock + +### 3.1 How it works + +A core middleware, mounted ahead of every router when `DEMO_MODE` is on, compares each write +request (`POST`/`PUT`/`PATCH`/`DELETE`) against a deny list of method + path patterns. A match +answers: + +``` +403 { "message": "This is turned off on the demo site.", "code": "DEMO_LOCKED" } +``` + +The web client shows the message as a toast wherever an action fails with that code. The Android +app maps the code to a new error kind with the same wording; today it would show the generic +"Something went wrong", which is misleading for a reviewer. + +Reads are never locked. Every page can be opened and every form filled in; only the save is +refused. That keeps the demo useful as a tour of the admin panel. + +Module routes need their own entries, because they are mounted by core but belong to the module. +§9 Q1 asks how the module declares them. + +### 3.2 What is locked + +Paths are relative to `/api/v1`. Tier A breaks the site for the next visitor and is always locked. Tier B reaches the real game +server, and Tier C is content anyone can see; both are §9 questions. + +**Tier A — always locked (core)** + +| Area | Routes | What a visitor could otherwise do | +| --- | --- | --- | +| Site mode | `PUT /admin/site-mode` | Put the site into maintenance; every non-admin is locked out | +| Settings & branding | `PUT /admin/settings`, `POST /admin/settings/brand-asset/:slot`, `DELETE /admin/settings/:key` | Rename or rebrand the site, swap the logo | +| Modules | every write under `/admin/modules` (install, sources, restart, enable, disable, purge, delete) | Disable or purge the Rust module; restart the server | +| SSO providers | writes under `/admin/auth/providers` | Add a sign-in provider pointing anywhere | +| Email | `PUT /admin/email/config`, `POST /admin/email/test`, `POST /admin/email/disconnect` | Connect a mail account; send mail from the site | +| Discord bot | `PUT /admin/discord-bot/config` | Point the bot at someone's server | +| 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 | +| Password reset | `POST /auth/password/forgot`, `POST /auth/password/reset/:token` | Reset a demo account's password | + +**Tier A — always locked (Rust module)** + +| Area | Routes | Why | +| --- | --- | --- | +| The link to the rig | `PUT`/`DELETE /admin/rust/servers/:id` | Repoint or delete the sidecar link; the site goes dark | +| Plugin config files | `POST /admin/rust/config/:serverId/file` | Writes a plugin's config file on the rig's disk | + +**Tier B — reaches the rig (§9 Q3).** Permission writes (policy, grant, revoke, groups, members, +drift actions, sync), RunicNPC writes (factions, profiles, placements, push, respawn), economics and +permission grants to a linked player, console-command steps and every other world action an event +can run, and map fetch/render. + +**Tier C — content anyone can see (§9 Q4).** Posts, wiki, pages, events, teams and team forums, +moderation actions, invites, uploads, and the public contact form. + +## 4. Static logins + +| Account | Role | Notes | +| --- | --- | --- | +| `demo-admin` | admin | The admin panel, read in full; writes per §3 | +| `demo-moderator` | moderator | Moderation queue, appeals | +| `demo-player` | player | Self-service; a linked Steam account so the Rust player screens have data (§9 Q6) | +| `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 +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. + +## 5. Outbound effects + +| Channel | On the demo | +| --- | --- | +| Email | Not configured. Registration closed, password reset locked, digest and engagement mail never send | +| Push (ntfy) | Not deployed. The app's notification settings still save; nothing is delivered | +| Discord bot | Not deployed | +| The rig | Per §9 Q3 | + +## 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. + +## 7. The wipe + +A scheduled workflow on the `demo-site` runner (§9 Q7 for the cadence) runs: + +1. `docker compose down -v` — drops the database **and** the uploads volume, so nothing a visitor + made survives. +2. `docker compose pull && docker compose up -d` — the app boots, creates the schema, and installs + the Rust module from `MODULES=rust@=` in `.env`. +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); + - 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 content: a few news posts, wiki pages, an upcoming event and a team, enough + for every screen to have something on it; + - stamps `resetsAt` for the banner. +5. Smoke check: `GET /api/v1/public/status` answers, and the Rust server reports a live connection. + +A wipe takes the site down for about a minute. Signed-in visitors, including app users, are signed +out: their sessions and refresh tokens were in the database. + +The same workflow runs on a push that changes the deploy directory and on `workflow_dispatch`, so a +redeploy is the same path as a wipe. + +## 8. Where it lives + +| Piece | Repo | +| --- | --- | +| `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) | +| The Rust module's own lock entries | `Module-Rust` (§9 Q1) | +| `DEMO_LOCKED` error kind and the banner in the app | `Android-app` | +| `docker-compose.yml`, `.env.example`, seed fixture, wipe workflow | §9 Q8 | +| `.env` (JWT secret, encryption key, the rig's sidecar token) | On the `demo-site` host only, never in a repo | + +## 9. Questions for the org lead + +**Q1 — How does the Rust module declare its locked routes?** +Example: the demo is up and `demo-admin` opens Rust → Servers → Edit and changes the sidecar URL. +Something has to know that route is locked. +- **(a) Recommended:** `module.json` gains an optional `demoLocks` list of method + path, read by + core's lock. A module with no list is unaffected. It is a new manifest field, so `MODULE_API` + gets a minor bump and the Integration Kit documents it. +- (b) Core hard-codes the Rust module's paths. No module change, but core then knows one module's + routes, which the module system was built to avoid. + +**Q2 — Is the Tier A list (§3.2) complete, and is anything on it you want left open?** +Example: should `demo-admin` be able to change the site's theme colours, which are under Settings? +It is harmless and resets at the wipe, but it is also the first thing a visitor would deface. + +**Q3 — What may a visitor do to the rig?** +Example: `demo-admin` grants `demo-player` a kit permission, or starts an event whose step runs +`server.time 0` on the Oxide rig. The website wipe does not undo either; the rig keeps them. +- **(a) Recommended:** lock everything that writes to the rig (all of Tier B). Visitors see the + live map, feed, players and every admin screen, but saves that would reach the rig are refused. +- (b) Allow a short list of harmless, self-healing actions — for example respawning a RunicNPC or + rendering the map — and lock the rest. +- (c) Allow everything and also reset the rig in the wipe. Much larger: a rig wipe means + permissions, NPC placements and plugin state, and a Rust server restart. + +**Q4 — May visitors publish content?** +Example: someone signs in as `demo-admin` and posts a news item with an offensive image or a +phishing link. It is public on `demo.runicgateway.com` until the next wipe. +- **(a) Recommended:** allow posts, wiki, pages, events and teams, so the demo shows the real + editors. Lock **uploads** (no hosting of arbitrary files on our domain) and the **contact form**. + Serve the demo with `noindex`, so nothing a visitor writes reaches search engines. +- (b) Lock all publishing as well; the editors open but every save is refused. +- (c) Allow uploads too, with the wipe as the only cleanup. + +**Q5 — Which rig does the demo use, and what happens to our walks on it?** +Example: next week a walk needs the Oxide rig while the demo is linked to it. The Rust module's +site-owned permission sync means two sites linked to one rig fight over its permissions. +- **(a) Recommended:** the Oxide rig becomes the demo's rig; walks use the Carbon rig. +- (b) A third rig for the demo. +- (c) Share it, with permission sync off on the demo. + +**Q6 — Does `demo-player` have a linked Steam account?** +Example: the player's Rust screens (stats, linked accounts, economics) are empty without one. +- **(a) Recommended:** the seed links a stand-in Steam ID that has played on the rig, so the screens + have data. It is not anyone's real account. +- (b) Leave it unlinked, and show the empty states. + +**Q7 — How often does it wipe?** +- **(a) Recommended:** every 2 hours, on the hour (00:00, 02:00, … UTC). Each wipe signs everyone + out, which a reviewer in the middle of a review may notice. +- (b) Every 6 hours, which leaves visitor changes up for longer. + +**Q8 — Where do the deployment files live?** +- **(a) Recommended:** `website/deploy/demo/` (compose file, `.env.example`, seed fixture) and + `website/.gitea/workflows/demo-wipe.yml`. The seed script ships in the image, so its fixture sits + next to it. +- (b) A new repo, `RunicGateway/demo-site`, which keeps the website repo free of one deployment's + files. + +**Q9 — Which version does the demo run?** +- **(a) Recommended:** whatever `website` `main` and the latest Rust module release are at each + wipe, so the demo always shows the current release. +- (b) Pinned versions in `.env`, moved by hand. + +**Not asked, assumed:** the host's reverse proxy and DNS for `demo.runicgateway.com`, and that the +`demo-site` host can reach the Oxide rig's sidecar over the network. These are for the org lead to +confirm, not to decide. + +## 10. Build order, once §9 is answered + +1. **Core** (`website`): the `DEMO_MODE` lock and its tests, the banner field, `seed:demo`, and the + OpenAPI note that a route may answer `DEMO_LOCKED`. One PR. +2. **Module** (`Module-Rust`): its lock entries (Q1), plus a kit note if the manifest changes. One PR. +3. **Clients**: the web banner and toast (in the core PR), and the `DEMO_LOCKED` error kind and + banner in `Android-app`. One PR. +4. **Deployment**: compose, fixture and wipe workflow (Q8); first deploy on `demo-site`; a walk of + every static login and every Tier A lock, in the browser and the app. One PR. +5. **Play**: the sign-in details in the Play Console point at `play-review`; + runicgateway.com `/beta` publishes the logins. -- 2.49.1 From c4a1a6494178bb8e1fddf017484a17ea8b3f4b4d Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 17:57:07 -0500 Subject: [PATCH 2/4] docs(website): record the demo-mode decisions DM1-DM10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org lead's answers of 2026-10-23: demoLocks in module.json, settings open, the rig open but reset each cycle through Pterodactyl, content open with uploads locked hard, the Oxide rig as the demo's, a stand-in Steam link, every 2 hours, files in website/, latest versions, and every lock at the server route level. Adds the rig reset (§7.1-7.2): the site's first sync imports what is on the rig (D198, D244), so the rig is restored from a baseline archive before the site relinks, and that first import is the reset. Adds the hard upload rule (any multipart body refused). Three follow-ups: F1 console-command steps, F2 "latest" across the bundle, F3 reading of DM2. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 2 +- website/DEMO_MODE.md | 283 ++++++++++++++++++++++++++++--------------- 2 files changed, 186 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 5b5c88c..3b876bc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | -| [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, awaiting answers** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | +| [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, decided 2026-10-23 (three follow-ups open)** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | | [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem | | [CLILOCS.md](website/CLILOCS.md) | UO's id → name table, so items have names. The shard decompresses and serves it over the bridge; the desktop conversion it replaced is gone | diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index 9b2cb6f..fd728f9 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -1,6 +1,7 @@ # Demo mode — `demo.runicgateway.com` -**Status:** design, waiting on the org lead's answers to §9. Nothing is built. +**Status:** design. The org lead answered §9 Q1-Q9 on 2026-10-23 (recorded as decisions DM1-DM10 +in §9); three follow-up questions in §9.2 remain. Nothing is built. **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. @@ -51,20 +52,43 @@ app maps the code to a new error kind with the same wording; today it would show Reads are never locked. Every page can be opened and every form filled in; only the save is refused. That keeps the demo useful as a tour of the admin panel. -Module routes need their own entries, because they are mounted by core but belong to the module. -§9 Q1 asks how the module declares them. +**The server is the lock, never the UI (DM10).** Every lock is enforced at the route, so a request +sent with `curl`, an old app build, or a hand-edited page is refused exactly like a click. The UI may +explain a lock (the toast, the banner) but never hides or disables a control as the means of +locking it, and no client-side check is relied on. -### 3.2 What is locked +Module routes are mounted by core but belong to the module, so the module declares its own locked +routes in `module.json` (DM1, §3.2). -Paths are relative to `/api/v1`. Tier A breaks the site for the next visitor and is always locked. Tier B reaches the real game -server, and Tier C is content anyone can see; both are §9 questions. +### 3.2 The module's declaration + +`module.json` gains an optional `demoLocks` list, read by core when `DEMO_MODE` is on: + +```json +"demoLocks": [ + { "method": "PUT", "path": "/admin/rust/servers/:id" }, + { "method": "DELETE", "path": "/admin/rust/servers/:id" }, + { "method": "POST", "path": "/admin/rust/config/:serverId/file" } +] +``` + +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). + +### 3.3 What is locked + +Paths are relative to `/api/v1`. **Tier A** would break the site for the next visitor and is always +locked. **Uploads** are locked hard. **Tier B** (the rig) and **Tier C** (content) stay **open**: the +wipe puts them back (§7). **Tier A — always locked (core)** | Area | Routes | What a visitor could otherwise do | | --- | --- | --- | | Site mode | `PUT /admin/site-mode` | Put the site into maintenance; every non-admin is locked out | -| Settings & branding | `PUT /admin/settings`, `POST /admin/settings/brand-asset/:slot`, `DELETE /admin/settings/:key` | Rename or rebrand the site, swap the logo | | Modules | every write under `/admin/modules` (install, sources, restart, enable, disable, purge, delete) | Disable or purge the Rust module; restart the server | | SSO providers | writes under `/admin/auth/providers` | Add a sign-in provider pointing anywhere | | Email | `PUT /admin/email/config`, `POST /admin/email/test`, `POST /admin/email/disconnect` | Connect a mail account; send mail from the site | @@ -82,13 +106,33 @@ server, and Tier C is content anyone can see; both are §9 questions. | The link to the rig | `PUT`/`DELETE /admin/rust/servers/:id` | Repoint or delete the sidecar link; the site goes dark | | Plugin config files | `POST /admin/rust/config/:serverId/file` | Writes a plugin's config file on the rig's disk | -**Tier B — reaches the rig (§9 Q3).** Permission writes (policy, grant, revoke, groups, members, -drift actions, sync), RunicNPC writes (factions, profiles, placements, push, respawn), economics and -permission grants to a linked player, console-command steps and every other world action an event -can run, and map fetch/render. +**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. -**Tier C — content anyone can see (§9 Q4).** Posts, wiki, pages, events, teams and team forums, -moderation actions, invites, uploads, and the public contact form. +**Uploads — locked hard (DM4)** + +| Route | What it stores | +| --- | --- | +| `POST /admin/uploads` | The media library | +| `POST /admin/posts/upload` | Images in posts | +| `POST /admin/settings/brand-asset/:slot` | Logo, hero, favicon | +| `POST /player/teams/:slug/forum/uploads` | Images in team forum posts | + +"Hard" means a second rule beside the list: with `DEMO_MODE` on, core refuses **any** request whose +body is `multipart/form-data`, before a router sees it. An upload route added later is locked +without anyone remembering to list it, and a module cannot open one. + +**Tier B — the rig: open, reset by the wipe (DM3).** Permission writes (policy, grant, revoke, +groups, members, drift actions, sync), RunicNPC writes (factions, profiles, placements, push, +respawn), economics and permission grants to a linked player, the world actions events run, and map +fetch/render all work. The rig starts every cycle from a **demo default set** of permissions, NPCs +and events, and the wipe restores it (§7.1). Console-command steps are the exception that needs a +rule of their own (§9.2 F1). + +**Tier C — content: open (DM4).** Posts, wiki, pages, events, teams and team forums, moderation +actions and invites work. The contact form is locked (`POST /public/contact`), because it sends mail +to a person. The demo is served with `X-Robots-Tag: noindex` and a matching `robots.txt`, so nothing +a visitor writes reaches a search engine. ## 4. Static logins @@ -96,7 +140,7 @@ moderation actions, invites, uploads, and the public contact form. | --- | --- | --- | | `demo-admin` | admin | The admin panel, read in full; writes per §3 | | `demo-moderator` | moderator | Moderation queue, appeals | -| `demo-player` | player | Self-service; a linked Steam account so the Rust player screens have data (§9 Q6) | +| `demo-player` | player | Self-service; linked to a stand-in Steam ID that has played on the rig, so the Rust player screens have data (DM6) | | `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`. @@ -111,7 +155,7 @@ address with (§5), and the static logins cover every role. | Email | Not configured. Registration closed, password reset locked, digest and engagement mail never send | | Push (ntfy) | Not deployed. The app's notification settings still save; nothing is delivered | | Discord bot | Not deployed | -| The rig | Per §9 Q3 | +| The rig | Open, and restored to the demo default set at every wipe (§7.1) | ## 6. The banner @@ -122,115 +166,158 @@ needs to know the schedule. ## 7. The wipe -A scheduled workflow on the `demo-site` runner (§9 Q7 for the cadence) runs: +A scheduled workflow on the `demo-site` runner runs **every 2 hours on the hour, UTC** (DM7). It +resets the rig first and the site second, and the order matters. -1. `docker compose down -v` — drops the database **and** the uploads volume, so nothing a visitor - made survives. -2. `docker compose pull && docker compose up -d` — the app boots, creates the schema, and installs - the Rust module from `MODULES=rust@=` in `.env`. +### 7.1 Why the rig goes first + +The site's first sync with a server **imports what it finds there**. A new site's first permission +read adopts every grant and group on the rig (D198), and RunicNPC profiles are adopted before the +first push (D244). So wiping only the site would bring back every change a visitor made on the rig, +now owned by the fresh site. The rig has to be back at the demo default set **before** the new site +first talks to it. Then that first import *is* the reset: the site adopts the default set, and the +default set never has to be described twice. + +### 7.2 The rig: restore a baseline through Pterodactyl (DM3) + +The demo rig is the existing `rust-oxide` Pterodactyl server (DM5). On it sits one archive, +`demo-baseline.tar.gz`, holding the demo default set: + +- `oxide/data/` — permissions and groups, RunicNPC profiles and placements, Economics balances, the + bridge plugin's own data; +- `oxide/config/` — plugin configs, so even a config that changed some other way comes back; +- `server//` — the world save and `cfg/`, so anything an event built or a console command + wrote with `server.writecfg` comes back too. + +`oxide/plugins/` is **not** in the baseline. Plugin versions are the deployment's business (§9.2 +F2), not the wipe's. + +The workflow uses the Pterodactyl **client** API (files and power): + +1. `POST …/power {"signal":"kill"}` — no save, since the world is about to be replaced. +2. `POST …/files/delete` the three directories. +3. `POST …/files/decompress` `demo-baseline.tar.gz` at the server root. +4. `POST …/power {"signal":"start"}`. Rust takes a few minutes to boot; the site does not wait. + +**Capturing the baseline** is a second, hand-run workflow (`workflow_dispatch` only): someone sets up +the default set through the demo site (the permission groups, a handful of NPCs, events), then runs +it. It saves the world (`server.save` over the console), stops the server, `POST …/files/compress`es +the three directories into `demo-baseline.tar.gz`, and starts the server again. Re-run it whenever +the default set should change. + +**The key.** Pterodactyl client keys belong to a user and reach every server that user can see. The +demo gets its own Pterodactyl **subuser**, with access to `rust-oxide` only and the file and power +permissions only, and that subuser's client key is the `demo-site` workflow's secret +(`PTERODACTYL_CLIENT_KEY`). The panel URL and server identifier are plain variables. + +### 7.3 The site + +1. `docker compose down -v` — drops the database **and** the uploads volume. +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@=` (DM9, §9.2 F2). 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); + - 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 content: a few news posts, wiki pages, an upcoming event and a team, enough - for every screen to have something on it; + - writes the seeded site content: news posts, wiki pages, the demo events and a team; - stamps `resetsAt` for the banner. -5. Smoke check: `GET /api/v1/public/status` answers, and the Rust server reports a live connection. +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 + reports a live connection. A rig that has not come back fails the run, and the site still + serves with the server shown offline. -A wipe takes the site down for about a minute. Signed-in visitors, including app users, are signed -out: their sessions and refresh tokens were in the database. +### 7.4 What a visitor sees -The same workflow runs on a push that changes the deploy directory and on `workflow_dispatch`, so a -redeploy is the same path as a wipe. +The site is down for about a minute; the rig for a few. Signed-in visitors, including app users, +are signed out, because their sessions and refresh tokens were in the database. + +The same workflow runs on `workflow_dispatch` and on a push to `main` that changes `deploy/demo/`, +so a redeploy is the same path as a wipe. ## 8. Where it lives | Piece | Repo | | --- | --- | | `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) | -| The Rust module's own lock entries | `Module-Rust` (§9 Q1) | +| The Rust module's `demoLocks`, and the console-command rule (§9.2 F1) | `Module-Rust` | | `DEMO_LOCKED` error kind and the banner in the app | `Android-app` | -| `docker-compose.yml`, `.env.example`, seed fixture, wipe workflow | §9 Q8 | +| `deploy/demo/` (compose file, `.env.example`, seed fixture) and `.gitea/workflows/demo-wipe.yml` + `demo-baseline.yml` | `website` (DM8) | | `.env` (JWT secret, encryption key, the rig's sidecar token) | On the `demo-site` host only, never in a repo | +| `PTERODACTYL_CLIENT_KEY` (the demo subuser's key) | A Gitea Actions secret on `website` | +| `demo-baseline.tar.gz` | On the rig, at the Pterodactyl server root | -## 9. Questions for the org lead +## 9. Decisions and open questions -**Q1 — How does the Rust module declare its locked routes?** -Example: the demo is up and `demo-admin` opens Rust → Servers → Edit and changes the sidecar URL. -Something has to know that route is locked. -- **(a) Recommended:** `module.json` gains an optional `demoLocks` list of method + path, read by - core's lock. A module with no list is unaffected. It is a new manifest field, so `MODULE_API` - gets a minor bump and the Integration Kit documents it. -- (b) Core hard-codes the Rust module's paths. No module change, but core then knows one module's - routes, which the module system was built to avoid. +### 9.1 Decisions (org lead, 2026-10-23) -**Q2 — Is the Tier A list (§3.2) complete, and is anything on it you want left open?** -Example: should `demo-admin` be able to change the site's theme colours, which are under Settings? -It is harmless and resets at the wipe, but it is also the first thing a visitor would deface. +| # | Question | Decision | +| --- | --- | --- | +| DM1 | How the module declares its locked routes | A `demoLocks` list in `module.json`; `MODULE_API` minor bump (§3.2) | +| DM2 | Is anything on Tier A to stay open? | Settings stay open (theme, navigation and the rest of `PUT /admin/settings`); the rest of Tier A stays locked (§3.3). See F3 | +| DM3 | What may a visitor do to the rig? | Permissions, NPCs and events start from a **demo default set**, can be changed through the site, and are put back each cycle by a CI script that restores the rig through Pterodactyl (§7.2) | +| DM4 | May visitors publish content? | Yes. **Uploads are locked hard** (§3.3); the contact form is locked; the site is `noindex` | +| DM5 | Which rig? | The existing Oxide rig becomes the demo's. A new Oxide server is spun up for testing when a walk needs one | +| DM6 | A Steam link for `demo-player`? | Yes, a stand-in Steam ID that has played on the rig | +| DM7 | Cadence | Every 2 hours | +| DM8 | Where the files live | `website/deploy/demo/` and `website/.gitea/workflows/` | +| DM9 | Version | Latest, at every wipe | +| DM10 | (the org lead, unprompted) | **Every lock is at the server route level, never the UI level** (§3.1) | -**Q3 — What may a visitor do to the rig?** -Example: `demo-admin` grants `demo-player` a kit permission, or starts an event whose step runs -`server.time 0` on the Oxide rig. The website wipe does not undo either; the rig keeps them. -- **(a) Recommended:** lock everything that writes to the rig (all of Tier B). Visitors see the - live map, feed, players and every admin screen, but saves that would reach the rig are refused. -- (b) Allow a short list of harmless, self-healing actions — for example respawning a RunicNPC or - rendering the map — and lock the rest. -- (c) Allow everything and also reset the rig in the wipe. Much larger: a rig wipe means - permissions, NPC placements and plugin state, and a Rust server restart. +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 +`X-Forwarded-*` by default (`TRUST_PROXY`), and the deploy PR checks secure cookies and client IPs +through that proxy. -**Q4 — May visitors publish content?** -Example: someone signs in as `demo-admin` and posts a news item with an offensive image or a -phishing link. It is public on `demo.runicgateway.com` until the next wipe. -- **(a) Recommended:** allow posts, wiki, pages, events and teams, so the demo shows the real - editors. Lock **uploads** (no hosting of arbitrary files on our domain) and the **contact form**. - Serve the demo with `noindex`, so nothing a visitor writes reaches search engines. -- (b) Lock all publishing as well; the editors open but every save is refused. -- (c) Allow uploads too, with the wipe as the only cleanup. +### 9.2 Follow-up questions -**Q5 — Which rig does the demo use, and what happens to our walks on it?** -Example: next week a walk needs the Oxide rig while the demo is linked to it. The Rust module's -site-owned permission sync means two sites linked to one rig fight over its permissions. -- **(a) Recommended:** the Oxide rig becomes the demo's rig; walks use the Carbon rig. -- (b) A third rig for the demo. -- (c) Share it, with permission sync off on the demo. +**F1 — Console-command steps on the demo.** +Example: `demo-admin` adds a console-command step with `quit` to an event and starts it. The Rust +server stops and stays down until the next wipe, up to two hours, for every visitor. Other commands +do the same more quietly: `oxide.unload RunicGateway` cuts the site off from the rig, and +`kickall` and `ban` affect anyone playing. The command box is free text with no allowlist today +(Rust §16). +- **(a) Recommended:** on the demo, the module accepts a command step or command reward only if its + command is on a short allowlist, for example `say`, `env.time`, `weather.*` and `server.time`. + The module checks it when the event is saved **and** when the step runs, so it is a server-side + rule (DM10). Everything else in events stays open. +- (b) Refuse command steps and command rewards entirely on the demo. +- (c) Leave them open; the wipe is the cleanup, and the rig may be down until it. -**Q6 — Does `demo-player` have a linked Steam account?** -Example: the player's Rust screens (stats, linked accounts, economics) are empty without one. -- **(a) Recommended:** the seed links a stand-in Steam ID that has played on the rig, so the screens - have data. It is not anyone's real account. -- (b) Leave it unlinked, and show the empty states. +**F2 — "Latest" across the website, the module and the rig.** +Example: Module-Rust ships a release that needs protocol 15 while the rig's bridge plugin still +speaks 14. At the next wipe, the demo pulls the new module, and the site refuses the rig until +someone updates the plugins. +- **(a) Recommended:** the demo follows the latest **Rust bundle**, the protocol-checked set of + module, sidecar and plugins that CI already publishes. When the bundle changes, the wipe also + puts the bundle's plugins and sidecar onto the rig through the same Pterodactyl file API. The + website image always follows `main`. +- (b) Latest module at every wipe, and the rig's plugins updated by hand when a release needs it. -**Q7 — How often does it wipe?** -- **(a) Recommended:** every 2 hours, on the hour (00:00, 02:00, … UTC). Each wipe signs everyone - out, which a reviewer in the middle of a review may notice. -- (b) Every 6 hours, which leaves visitor changes up for longer. +**F3 — DM2, read correctly?** +"Stay open" is read as: the settings (theme, colours, navigation, site texts) stay open, and every +other Tier A row stays locked, including site mode, modules, SSO, email, Discord, the demo +accounts' credentials and 2FA, and the rig link. Is that right? -**Q8 — Where do the deployment files live?** -- **(a) Recommended:** `website/deploy/demo/` (compose file, `.env.example`, seed fixture) and - `website/.gitea/workflows/demo-wipe.yml`. The seed script ships in the image, so its fixture sits - next to it. -- (b) A new repo, `RunicGateway/demo-site`, which keeps the website repo free of one deployment's - files. +**For the org lead to set up (not questions):** a Pterodactyl subuser with access to `rust-oxide` +only (file and power permissions), and its client key as the `PTERODACTYL_CLIENT_KEY` secret on +`website` (§7.2). Also confirm that the `demo-site` host can reach the Pterodactyl panel and the +rig's sidecar over the network. -**Q9 — Which version does the demo run?** -- **(a) Recommended:** whatever `website` `main` and the latest Rust module release are at each - wipe, so the demo always shows the current release. -- (b) Pinned versions in `.env`, moved by hand. +## 10. Build order, once §9.2 is answered -**Not asked, assumed:** the host's reverse proxy and DNS for `demo.runicgateway.com`, and that the -`demo-site` host can reach the Oxide rig's sidecar over the network. These are for the org lead to -confirm, not to decide. - -## 10. Build order, once §9 is answered - -1. **Core** (`website`): the `DEMO_MODE` lock and its tests, the banner field, `seed:demo`, and the - OpenAPI note that a route may answer `DEMO_LOCKED`. One PR. -2. **Module** (`Module-Rust`): its lock entries (Q1), plus a kit note if the manifest changes. One PR. -3. **Clients**: the web banner and toast (in the core PR), and the `DEMO_LOCKED` error kind and - banner in `Android-app`. One PR. -4. **Deployment**: compose, fixture and wipe workflow (Q8); first deploy on `demo-site`; a walk of - every static login and every Tier A lock, in the browser and the app. One PR. -5. **Play**: the sign-in details in the Play Console point at `play-review`; +1. **Core** (`website`): the `DEMO_MODE` lock (deny list, the `multipart/form-data` rule, the + demo-account and other-session checks), `demoLocks` loading, the banner field, `seed:demo`, + `noindex`, the web banner and toast, `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. +2. **Module** (`Module-Rust`): `demoLocks` in `module.json`, and the F1 rule with its tests. One PR. +3. **Kit** (`Integration-kit`): the `demoLocks` paragraph 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. +5. **Deployment** (`website`): `deploy/demo/`, `demo-wipe.yml` and `demo-baseline.yml`; the first + deploy on `demo-site`; setting up the default set and capturing the baseline; then a walk of + every static login, every Tier A lock and the upload lock, in the browser and the app, plus + one full wipe cycle that restores a changed permission, NPC and event. One PR. +6. **Play**: the sign-in details in the Play Console point at `play-review`; runicgateway.com `/beta` publishes the logins. -- 2.49.1 From 5f3c88e9800aacd6da1ca099f11a2f4f37b24144 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 18:09:02 -0500 Subject: [PATCH 3/4] =?UTF-8?q?docs(website):=20demo=20mode=20fully=20deci?= =?UTF-8?q?ded=20=E2=80=94=20deny=20list,=20bundle,=20setup=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DM11: console-command steps keep running on the demo except a deny list (quit/restart, oxide.unload/reload, rcon.*, fps.limit, server.tickrate), matched on the normalised first word (global. stripped, o. mapped to oxide.) and enforced by the module at save and at run behind a new ctx.demoMode. DM12: "latest" follows the Rust bundle for the rig's plugins and sidecar, the newest Module-Rust release on the same protocol (a new protocol field in its install manifest, which core's installer ignores), and the website main image. If nothing matches yet, the wipe keeps the previous set. DM13: only settings stay open. The org lead created the Pterodactyl subuser and added PTERODACTYL_CLIENT_KEY. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 2 +- website/DEMO_MODE.md | 156 ++++++++++++++++++++++++++++--------------- 2 files changed, 102 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 3b876bc..a4b9b1c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | -| [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, decided 2026-10-23 (three follow-ups open)** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | +| [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, fully decided 2026-10-23** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | | [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem | | [CLILOCS.md](website/CLILOCS.md) | UO's id → name table, so items have names. The shard decompresses and serves it over the bridge; the desktop conversion it replaced is gone | diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index fd728f9..c18c136 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -1,7 +1,7 @@ # Demo mode — `demo.runicgateway.com` -**Status:** design. The org lead answered §9 Q1-Q9 on 2026-10-23 (recorded as decisions DM1-DM10 -in §9); three follow-up questions in §9.2 remain. Nothing is built. +**Status:** design, **fully decided** 2026-10-23 (DM1-DM13, §9). Ready to build per §10. Nothing is +built yet. **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. @@ -126,14 +126,46 @@ without anyone remembering to list it, and a module cannot open one. groups, members, drift actions, sync), RunicNPC writes (factions, profiles, placements, push, respawn), economics and permission grants to a linked player, the world actions events run, and map fetch/render all work. The rig starts every cycle from a **demo default set** of permissions, NPCs -and events, and the wipe restores it (§7.1). Console-command steps are the exception that needs a -rule of their own (§9.2 F1). +and events, and the wipe restores it (§7.1). Console-command steps stay open too, except for the +commands on a deny list (§3.4, DM11). **Tier C — content: open (DM4).** Posts, wiki, pages, events, teams and team forums, moderation actions and invites work. The contact form is locked (`POST /public/contact`), because it sends mail to a person. The demo is served with `X-Robots-Tag: noindex` and a matching `robots.txt`, so nothing a visitor writes reaches a search engine. +### 3.4 The console-command deny list (DM11) + +Console-command event steps and command rewards are a free-text box with no allowlist (Rust §16). +On the demo, the Rust module refuses a command on this **deny list**, which holds the commands that +would break the rig for every visitor until the next wipe. Everything else runs. + +| Command (any of these spellings) | Why it is denied | +| --- | --- | +| `quit`, `restart` | Stops or restarts RustDedicated. The rig stays down, or the site loses it for minutes, until the wipe | +| `oxide.unload`, `oxide.reload` (with any argument, `*` included) | Unloads `RunicGateway`, which cuts the site off from the rig, or `RunicNPC`, `Kits`, `ZoneManager` and the rest, which the bridge needs | +| `rcon.*` | The panel's console runs over RCON, and the wipe's baseline capture uses that console | +| `fps.limit`, `server.tickrate` | Starves the server tick; the rig crawls for every visitor | + +**Matching.** The check normalises the command before comparing: it trims it and lower-cases it, +strips a leading `global.` (Rust accepts `global.quit`), and maps the `o.` short form to `oxide.` +(`o.unload`). It compares the **first word** only, so the arguments cannot hide a denied command. +`;` does not split console commands in Rust (the §16 spike), so there is no second command to look +for. + +**Where it is enforced (DM10).** In the Rust module's server code, at two points: +- when an event or reward holding the command is **saved**, answering `403 DEMO_LOCKED` with the + 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. + +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 +back (§7.2). + ## 4. Static logins | Account | Role | Notes | @@ -189,8 +221,8 @@ The demo rig is the existing `rust-oxide` Pterodactyl server (DM5). On it sits o - `server//` — the world save and `cfg/`, so anything an event built or a console command wrote with `server.writecfg` comes back too. -`oxide/plugins/` is **not** in the baseline. Plugin versions are the deployment's business (§9.2 -F2), not the wipe's. +`oxide/plugins/` and the sidecar are **not** in the baseline. Their versions follow the Rust bundle +(§7.5, DM12), not the snapshot. The workflow uses the Pterodactyl **client** API (files and power): @@ -208,13 +240,14 @@ the default set should change. **The key.** Pterodactyl client keys belong to a user and reach every server that user can see. The demo gets its own Pterodactyl **subuser**, with access to `rust-oxide` only and the file and power permissions only, and that subuser's client key is the `demo-site` workflow's secret -(`PTERODACTYL_CLIENT_KEY`). The panel URL and server identifier are plain variables. +(`PTERODACTYL_CLIENT_KEY`). The panel URL and server identifier are plain variables. **Done:** the +org lead created the subuser and added the secret on 2026-10-23. ### 7.3 The site 1. `docker compose down -v` — drops the database **and** the uploads volume. 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@=` (DM9, §9.2 F2). + the schema, and installs the Rust module from `MODULES=rust@=`, 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: @@ -237,26 +270,58 @@ are signed out, because their sessions and refresh tokens were in the database. The same workflow runs on `workflow_dispatch` and on a push to `main` that changes `deploy/demo/`, so a redeploy is the same path as a wipe. +### 7.5 Versions: the latest Rust bundle (DM9, DM12) + +"Latest" is decided per piece, so the three halves of the Rust link can never disagree on protocol: + +| Piece | Follows | +| --- | --- | +| Website image | `website-app:latest`, the newest `main` | +| Rig plugins + sidecar | The Rust bundle's `current.json` (`installer`, branch `bundles`, `v2/rust/`). A bundle is the protocol-checked pair of `Rust-Link` and `Rust-Plugins` that CI already publishes | +| Rust module | The newest `Module-Rust` release whose `protocol` **equals** that bundle's `protocol` | + +The Rust game link has no `409`: a plugin on a different protocol would mis-parse, not be refused. +That is why the module is chosen by protocol, not by date. + +**What the wipe adds.** Before §7.2's rig restore, the workflow reads `current.json`: + +1. **The bundle changed since the last wipe.** It downloads the bundle's plugin tarball and sidecar + binary, checks each `sha256`, and writes them onto the rig through the same Pterodactyl file API + while the server is stopped. That means the plugin files into `oxide/plugins/`, and the sidecar + and its `with-sidecar.sh` launcher where the egg expects them. It records the bundle tag on the + `demo-site` host. +2. **Always.** It picks the module release as above, and writes `MODULES=rust@=` + into the deploy directory's `.env` before §7.3. + +`Module-Rust`'s release CI adds `protocol` (read from `PROTOCOL_VERSION` in +`server/sidecarClient.js`) to the install manifest it already publishes, so the choice needs no +guessing. Core's installer reads only the fields it names (`modules/install.js`), so the extra +field needs no core change. + +If no module release matches the new bundle yet, the wipe **keeps the previous bundle and module**, +finishes the reset on them, and warns in the run. A half-released protocol bump never takes the demo +down. + ## 8. Where it lives | Piece | Repo | | --- | --- | | `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) | -| The Rust module's `demoLocks`, and the console-command rule (§9.2 F1) | `Module-Rust` | +| The Rust module's `demoLocks`, the console-command deny list (§3.4), and `protocol` in its install manifest (§7.5) | `Module-Rust` | | `DEMO_LOCKED` error kind and the banner in the app | `Android-app` | | `deploy/demo/` (compose file, `.env.example`, seed fixture) and `.gitea/workflows/demo-wipe.yml` + `demo-baseline.yml` | `website` (DM8) | | `.env` (JWT secret, encryption key, the rig's sidecar token) | On the `demo-site` host only, never in a repo | | `PTERODACTYL_CLIENT_KEY` (the demo subuser's key) | A Gitea Actions secret on `website` | | `demo-baseline.tar.gz` | On the rig, at the Pterodactyl server root | -## 9. Decisions and open questions +## 9. Decisions ### 9.1 Decisions (org lead, 2026-10-23) | # | Question | Decision | | --- | --- | --- | | DM1 | How the module declares its locked routes | A `demoLocks` list in `module.json`; `MODULE_API` minor bump (§3.2) | -| DM2 | Is anything on Tier A to stay open? | Settings stay open (theme, navigation and the rest of `PUT /admin/settings`); the rest of Tier A stays locked (§3.3). See F3 | +| DM2 | Is anything on Tier A to stay open? | Settings stay open (theme, navigation and the rest of `PUT /admin/settings`); the rest of Tier A stays locked (§3.3). Confirmed as "settings only" (DM13) | | DM3 | What may a visitor do to the rig? | Permissions, NPCs and events start from a **demo default set**, can be changed through the site, and are put back each cycle by a CI script that restores the rig through Pterodactyl (§7.2) | | DM4 | May visitors publish content? | Yes. **Uploads are locked hard** (§3.3); the contact form is locked; the site is `noindex` | | DM5 | Which rig? | The existing Oxide rig becomes the demo's. A new Oxide server is spun up for testing when a walk needs one | @@ -265,59 +330,40 @@ so a redeploy is the same path as a wipe. | DM8 | Where the files live | `website/deploy/demo/` and `website/.gitea/workflows/` | | DM9 | Version | Latest, at every wipe | | DM10 | (the org lead, unprompted) | **Every lock is at the server route level, never the UI level** (§3.1) | +| DM11 | (F1) Console-command steps on the demo | A **deny list** of the commands that would break the rig (`quit`, `oxide.unload RunicGateway` and the like), not an allowlist; everything else runs (§3.4) | +| 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 | 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 `X-Forwarded-*` by default (`TRUST_PROXY`), and the deploy PR checks secure cookies and client IPs through that proxy. -### 9.2 Follow-up questions +### 9.2 Set up by the org lead -**F1 — Console-command steps on the demo.** -Example: `demo-admin` adds a console-command step with `quit` to an event and starts it. The Rust -server stops and stays down until the next wipe, up to two hours, for every visitor. Other commands -do the same more quietly: `oxide.unload RunicGateway` cuts the site off from the rig, and -`kickall` and `ban` affect anyone playing. The command box is free text with no allowlist today -(Rust §16). -- **(a) Recommended:** on the demo, the module accepts a command step or command reward only if its - command is on a short allowlist, for example `say`, `env.time`, `weather.*` and `server.time`. - The module checks it when the event is saved **and** when the step runs, so it is a server-side - rule (DM10). Everything else in events stays open. -- (b) Refuse command steps and command rewards entirely on the demo. -- (c) Leave them open; the wipe is the cleanup, and the rig may be down until it. +- **Done 2026-10-23:** the Pterodactyl subuser (access to `rust-oxide` only, file and power + permissions) and its client key as the `PTERODACTYL_CLIENT_KEY` Actions secret (§7.2). +- **In progress:** DNS and HTTPS through the external reverse proxy. +- **To confirm in the deploy phase:** the `demo-site` host reaches the Pterodactyl panel and the rig's + sidecar over the network. -**F2 — "Latest" across the website, the module and the rig.** -Example: Module-Rust ships a release that needs protocol 15 while the rig's bridge plugin still -speaks 14. At the next wipe, the demo pulls the new module, and the site refuses the rig until -someone updates the plugins. -- **(a) Recommended:** the demo follows the latest **Rust bundle**, the protocol-checked set of - module, sidecar and plugins that CI already publishes. When the bundle changes, the wipe also - puts the bundle's plugins and sidecar onto the rig through the same Pterodactyl file API. The - website image always follows `main`. -- (b) Latest module at every wipe, and the rig's plugins updated by hand when a release needs it. +## 10. Build order -**F3 — DM2, read correctly?** -"Stay open" is read as: the settings (theme, colours, navigation, site texts) stay open, and every -other Tier A row stays locked, including site mode, modules, SSO, email, Discord, the demo -accounts' credentials and 2FA, and the rig link. Is that right? - -**For the org lead to set up (not questions):** a Pterodactyl subuser with access to `rust-oxide` -only (file and power permissions), and its client key as the `PTERODACTYL_CLIENT_KEY` secret on -`website` (§7.2). Also confirm that the `demo-site` host can reach the Pterodactyl panel and the -rig's sidecar over the network. - -## 10. Build order, once §9.2 is answered - -1. **Core** (`website`): the `DEMO_MODE` lock (deny list, the `multipart/form-data` rule, the - demo-account and other-session checks), `demoLocks` loading, the banner field, `seed:demo`, - `noindex`, the web banner and toast, `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. -2. **Module** (`Module-Rust`): `demoLocks` in `module.json`, and the F1 rule with its tests. One PR. -3. **Kit** (`Integration-kit`): the `demoLocks` paragraph and the `core-ref.json` re-pin. One PR. +1. **Core** (`website`): the `DEMO_MODE` lock (the deny list, the `multipart/form-data` rule, the + 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. +2. **Module** (`Module-Rust`): `demoLocks` in `module.json`, the console-command deny list behind + `ctx.demoMode` (§3.4), and `protocol` in the release's install manifest (§7.5), each with + tests. One PR. +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. -5. **Deployment** (`website`): `deploy/demo/`, `demo-wipe.yml` and `demo-baseline.yml`; the first - deploy on `demo-site`; setting up the default set and capturing the baseline; then a walk of - every static login, every Tier A lock and the upload lock, in the browser and the app, plus - one full wipe cycle that restores a changed permission, NPC and event. One PR. +5. **Deployment** (`website`): `deploy/demo/`, `demo-wipe.yml` (with §7.5's bundle step) and + `demo-baseline.yml`; the first deploy on `demo-site`; setting up the default set and capturing + the baseline. Then a walk of every static login, every Tier A lock and the upload lock, in the + browser and the app, plus one full wipe cycle that restores a changed permission, NPC and + event, and a denied console command refused at save and at run. One PR. 6. **Play**: the sign-in details in the Play Console point at `play-review`; runicgateway.com `/beta` publishes the logins. -- 2.49.1 From aadc13f6f4c7284f872630ad232871e3905917b8 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 18:15:10 -0500 Subject: [PATCH 4/4] =?UTF-8?q?docs(website):=20DM14=20=E2=80=94=20open=20?= =?UTF-8?q?the=20plugin=20config=20editor,=20hide=20the=20bridge's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visitors may read and change every plugin config except the bridge's: the editor's save reloads through the bridge (with rollback, D178/D179), not the console, and the wipe restores oxide/config. The RunicGateway plugin's files are refused for reading as well as writing and left out of the file list, because they hold credentials for talking to the sidecar directly. Because the editor masks secret fields only in the form today, the demo also redacts flagged values server-side, makes them read-only, refuses raw-JSON saves, and leaves the sidecar URL out of the Servers admin view. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- website/DEMO_MODE.md | 56 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index c18c136..b851b0a 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -1,6 +1,6 @@ # Demo mode — `demo.runicgateway.com` -**Status:** design, **fully decided** 2026-10-23 (DM1-DM13, §9). Ready to build per §10. Nothing is +**Status:** design, **fully decided** 2026-10-23 (DM1-DM14, §9). Ready to build per §10. Nothing is built yet. **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 @@ -49,8 +49,8 @@ The web client shows the message as a toast wherever an action fails with that c app maps the code to a new error kind with the same wording; today it would show the generic "Something went wrong", which is misleading for a reviewer. -Reads are never locked. Every page can be opened and every form filled in; only the save is -refused. That keeps the demo useful as a tour of the admin panel. +Reads are never locked, with one exception: the bridge's config (§3.5). Every page can be opened +and every form filled in; only the save is refused. That keeps the demo useful as a tour of the admin panel. **The server is the lock, never the UI (DM10).** Every lock is enforced at the route, so a request sent with `curl`, an old app build, or a hand-edited page is refused exactly like a click. The UI may @@ -104,7 +104,7 @@ wipe puts them back (§7). | Area | Routes | Why | | --- | --- | --- | | The link to the rig | `PUT`/`DELETE /admin/rust/servers/:id` | Repoint or delete the sidecar link; the site goes dark | -| Plugin config files | `POST /admin/rust/config/:serverId/file` | Writes a plugin's config file on the rig's disk | +| 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. @@ -166,6 +166,41 @@ Commands that only change the world or the players (`server.writecfg`, `entity.* `weather.*`, `say`, `kick`, `ban`, `oxide.grant`) stay open. The baseline restore puts all of them back (§7.2). +### 3.5 The plugin config editor (DM14) + +Every plugin's config **except the bridge's** can be read and changed on the demo. A visitor can tune +RunicNPC, Kits, Economics and the rest and watch the change take effect. Three things make that safe: + +- **The save reloads through the bridge, not the console.** The bridge backs up the file, writes + it, reloads the plugin that owns it, and puts the old file back if that plugin fails to load (D178, + D179). The §3.4 deny list never sees this reload, and a config that breaks its plugin undoes + itself at once. +- **The wipe restores `oxide/config/`** from the baseline (§7.2), so a change lasts two hours at + most. +- **The bridge's own config is out of reach.** The editor's routes refuse every file the + `RunicGateway` plugin owns, for reading as well as writing, and the file list leaves those files + out. This is the one place where §3.1's "reads are never locked" gives way. The file holds the + bridge's link to the sidecar and its credentials, and anyone holding them could talk to the + sidecar directly over HTTP, outside every lock in this document. + +**Secrets in other plugins' configs.** A plugin config can hold a webhook URL or an API key. The +editor already flags those fields by key name (`SECRET_WORDS` in `configEdit.js`), but today it +returns the full text and masks them only **in the form**, behind a reveal button. Under DM10 that +mask is not a lock. On the demo, the server therefore: + +- **redacts** every flagged value in `GET …/file` and in the write history before it leaves the + module, replacing it with a placeholder; +- makes flagged fields **read-only**: a form save that edits one is refused; +- refuses **raw-JSON saves** (`text` instead of `edits`), because the raw document a visitor sends + back would hold the placeholders and overwrite the real values. Form saves splice only the fields + that changed, so untouched secrets stay as they are on disk. + +**The link on the Servers screen.** The module's admin view of a server never returns the sidecar +token; it returns `hasToken`. On the demo it also leaves out the sidecar's **URL**, so the screen +gives away nothing that helps someone reach the sidecar. Editing the link stays locked (§3.3). + +All of this is enforced in the module's server code behind `ctx.demoMode` (DM10), with tests. + ## 4. Static logins | Account | Role | Notes | @@ -307,7 +342,7 @@ down. | Piece | Repo | | --- | --- | | `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) | -| The Rust module's `demoLocks`, the console-command deny list (§3.4), and `protocol` in its install manifest (§7.5) | `Module-Rust` | +| The Rust module's `demoLocks`, the console-command deny list (§3.4), the config-editor rules (§3.5), and `protocol` in its install manifest (§7.5) | `Module-Rust` | | `DEMO_LOCKED` error kind and the banner in the app | `Android-app` | | `deploy/demo/` (compose file, `.env.example`, seed fixture) and `.gitea/workflows/demo-wipe.yml` + `demo-baseline.yml` | `website` (DM8) | | `.env` (JWT secret, encryption key, the rig's sidecar token) | On the `demo-site` host only, never in a repo | @@ -333,6 +368,7 @@ down. | DM11 | (F1) Console-command steps on the demo | A **deny list** of the commands that would break the rig (`quit`, `oxide.unload RunicGateway` and the like), not an allowlist; everything else runs (§3.4) | | 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) | 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 @@ -354,9 +390,9 @@ through that proxy. `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. -2. **Module** (`Module-Rust`): `demoLocks` in `module.json`, the console-command deny list behind - `ctx.demoMode` (§3.4), and `protocol` in the release's install manifest (§7.5), each with - tests. One PR. +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. 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. @@ -364,6 +400,8 @@ through that proxy. `demo-baseline.yml`; the first deploy on `demo-site`; setting up the default set and capturing the baseline. Then a walk of every static login, every Tier A lock and the upload lock, in the browser and the app, plus one full wipe cycle that restores a changed permission, NPC and - event, and a denied console command refused at save and at run. One PR. + event; a denied console command refused at save and at run; and a plugin config changed and + reloaded through the editor, with the bridge's config refused for reading and another plugin's + secret field redacted. One PR. 6. **Play**: the sign-in details in the Play Console point at `play-review`; runicgateway.com `/beta` publishes the logins. -- 2.49.1