docs(website): DM14 — open the plugin config editor, hide the bridge's

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-23 18:15:10 -05:00
parent 5f3c88e980
commit aadc13f6f4

View File

@@ -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.