docs(website): demo mode fully decided — deny list, bundle, setup done
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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/<identity>/` — 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@<version>=<manifest>` (DM9, §9.2 F2).
|
||||
the schema, and installs the Rust module from `MODULES=rust@<version>=<manifest>`, which the workflow writes (§7.5).
|
||||
3. Wait for the app's healthcheck.
|
||||
4. `docker compose exec app npm run seed:demo` — one core script that, from a fixture file in the
|
||||
deploy directory:
|
||||
@@ -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@<version>=<manifest>`
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user