docs(website): design demo mode for demo.runicgateway.com #377
@@ -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