docs(website): demo DM25 + DM26, first wipes, default set, public URL #386
@@ -1834,7 +1834,10 @@ it.
|
||||
A sentence back refuses the write `403 { code: "DEMO_LOCKED", message, errors }` with that sentence; a
|
||||
hook that throws refuses too.
|
||||
- **Uploads.** Any request with a `multipart/*` content type is refused app-wide, before a router or
|
||||
multer sees it.
|
||||
multer sees it. So is any write whose JSON body has a `data:` URI in any string (DM25): a pasted
|
||||
image is an upload that did not use multipart. The string is read as a browser reads an attribute
|
||||
(character references decoded, tabs and newlines removed); "metadata:" and prose such as
|
||||
"Data: none, yet" pass. Matching rules are in DEMO_MODE.md §3.3.
|
||||
- **Search engines.** Every response carries `X-Robots-Tag: noindex, nofollow`. `/robots.txt` is
|
||||
`Disallow: /`, served ahead of the SPA's static file.
|
||||
- **`GET /public/settings`** gains `demo: { resetsAt, logins }`, from the `demo_state` settings row.
|
||||
|
||||
@@ -151,6 +151,20 @@ next wipe.
|
||||
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.
|
||||
|
||||
**A pasted image is an upload too (DM25).** An image pasted into rich text (a post, a wiki page, an
|
||||
event description, a forum post, the hero draft) does not use multipart. It travels inside the
|
||||
JSON body as a `data:` URI. So with `DEMO_MODE` on, core also refuses **any write**
|
||||
(`POST`/`PUT`/`PATCH`/`DELETE`) whose JSON body carries a `data:` URI in **any string**, on any
|
||||
route, core's or a module's, before a router sees it. As with multipart, nothing is listed, so a
|
||||
rich-text field added later is covered without anyone remembering it. Every `data:` URI counts,
|
||||
not only `data:image/*`. A match is `data:` that is not the end of a longer word (so "metadata:"
|
||||
does not match), followed by a comma, a `;`, or a `type/subtype` and then a comma. So prose such
|
||||
as "Data: none, yet" saves. The string is first read as a browser reads an attribute: HTML
|
||||
character references are decoded and tabs and newlines removed, so `data:` and a `data:`
|
||||
split by a newline are still refused. Reads are never checked. (Off the demo, the server's HTML
|
||||
sanitizer already drops `data:` from `<img>` on news, wiki and forum bodies, but not on every
|
||||
rich-text field. The demo rule is the one rule that covers them all.)
|
||||
|
||||
**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
|
||||
@@ -287,6 +301,14 @@ now owned by the fresh site. The rig has to be back at the demo default set **be
|
||||
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.
|
||||
|
||||
**Except for two parts of it (DM26, found on the first restored wipes, 2026-10-27).** Permissions,
|
||||
groups and members come back through the import as described. **NPC profiles and factions** do not:
|
||||
the restored rig's RunicNPC says it is already managed, by the site before this one, so the new site
|
||||
adopts nothing (D244), and its first push sends an empty profile set. **Group chat styles** are
|
||||
never adopted by the import. These two come from the fixture instead (§7.3): the seed writes them
|
||||
into the site before its first sync, the site pushes them, and the baseline's NPC placements spawn
|
||||
again. The baseline still holds the placements, routes and everything else on the rig.
|
||||
|
||||
### 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,
|
||||
@@ -347,13 +369,19 @@ current `rust-oxide`. **Done:** the org lead created the subuser and added the s
|
||||
this only on the first run against a database, so a re-run never duplicates it;
|
||||
- hands each installed module its `modules.<id>` section through the module's `onDemoSeed` hook
|
||||
(DM15, `MODULE_API` 1.18.0). The Rust module writes its own server row, with the rig's sidecar URL
|
||||
and token, and `demo-player`'s stand-in Steam link (DM6). Core never writes a module's tables;
|
||||
and token, and `demo-player`'s stand-in Steam link (DM6), and the default set's NPC faction
|
||||
table, NPC profiles and group chat styles (DM26, §7.1). Core never writes a module's tables;
|
||||
- stamps `demo_state`: `resetsAt`, the next `DEMO_RESET_EVERY_HOURS` slot (default 2) on the hour
|
||||
UTC, and the published logins.
|
||||
|
||||
The fixture is JSON: `{ accounts, settings?, content?: { posts, wiki, pages, events }, modules? }`.
|
||||
The Rust module's section is `{ servers: [{ id, name, sidecarBaseUrl, sidecarToken, protocol?,
|
||||
enabled?, sortOrder? }], links: [{ username, steamId, name?, serverId? }] }`.
|
||||
enabled?, sortOrder? }], links: [{ username, steamId, name?, serverId? }], npcs?: { factions?:
|
||||
[{ a, b, relation }], profiles?: [{ name, servers?, allServers?, killsScope?, body }] }, chat?:
|
||||
[{ serverId, group, style }] }`. A profile's `body` lists only what differs from RunicNPC's defaults,
|
||||
and is checked as the admin page checks one. A `style` lists only the BetterChat fields that differ
|
||||
from the defaults. Its group is created on the site for that server if the import has not brought
|
||||
it in yet; the import then fills in the rig's title, rank and permissions.
|
||||
Any string `"$env:NAME"` in it is replaced with that variable from the host's `.env`, so the
|
||||
sidecar token is never in the repo. An unset variable fails the run instead of seeding an empty
|
||||
credential. The script checks the fixture before writing anything. It must name exactly the four
|
||||
@@ -363,8 +391,9 @@ current `rust-oxide`. **Done:** the org lead created the subuser and added the s
|
||||
|
||||
**There is no seeded team.** Core's Teams are synced from the game (a Rust clan becomes a Team), so
|
||||
the demo's team comes from a clan in the rig's baseline (§7.2), not from the fixture.
|
||||
5. The module's sync connects when the rig finishes booting, and its first import adopts the
|
||||
baseline's permissions and NPCs (§7.1).
|
||||
5. The module's sync connects when the rig finishes booting. Its first import adopts the
|
||||
baseline's permissions, and its first NPC push sends the seeded profiles, which the baseline's
|
||||
placements spawn from (§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.
|
||||
@@ -456,6 +485,8 @@ down.
|
||||
| DM22 | (build, step 5) The subuser has file and power permissions only, so `server.save` over the console is out of reach. How does the baseline capture save the world? | **A power `stop`**, which runs the egg's `quit` and writes the save; no console permission (§7.2) |
|
||||
| DM23 | (build, step 5) Where does the demo live on its host? | **`/opt/demo.runicgateway.com`**, the `demo-site` runner in host mode, like `rgcom`'s `/opt/runicgateway.com`. The org lead writes `.env` from `deploy/demo/.env.example` |
|
||||
| DM24 | (build, step 5) The module's demo code was on `edge` only, so no release could be installed. Cut over? | **Yes, now:** `Module-Rust` `edge` → `main` releases v0.6.0, the first with `protocol` in its install manifest |
|
||||
| DM25 | (amendment, 2026-10-27) Uploads are locked, but an image pasted into rich text is saved as a `data:` URI inside the JSON body. Block it? | **Yes, on every write and for every `data:` URI.** Core refuses any write whose JSON body has a `data:` URI in any string, on any route, the same way the multipart rule needs no list (§3.3) |
|
||||
| DM26 | (build, step 5, 2026-10-27) The restored rig is marked managed, so a fresh site adopts none of its NPC profiles and empties them on its first push; chat styles are never adopted. How does the default set bring them back? | **Seed them.** Module-Rust's `onDemoSeed` also takes `npcs` (factions, profiles) and `chat` (a style per group), written before the site's first sync (§7.1, §7.3). Patch release v0.6.1 |
|
||||
|
||||
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
|
||||
@@ -517,5 +548,17 @@ through that proxy.
|
||||
module at 0.6.0) ran locally against an image built from `main`; `rig.mjs plan` and `smoke` ran
|
||||
against the real Gitea; the Pterodactyl file calls ran against `egg-oxide` (§7.2). The first
|
||||
deploy, the baseline and the walk wait on the host's `.env` and `Module-Rust` v0.6.0.
|
||||
**First wipes 2026-10-27:** the host's `.env` was written and v0.6.0 released. Wipe 358's rig
|
||||
half worked (bundle 2026.10.22 written, rig restarted), but the seed loaded no module: the
|
||||
runner is not root, so its `chown` of `modules/` and `logs/` failed silently. The wipe now does
|
||||
the chown in a root container. The same day, DM25 added the `data:` URI rule (§3.3) to the walk's
|
||||
upload checks. Wipe 362 was the first green one. **The default set** was then built through the
|
||||
site as `demo-admin`. It holds `vip` and a new `moderator` group with chat titles, `demo-player` in
|
||||
`vip`, bandit and guard NPC profiles with an allied faction pair, and `guard-1` at the bandit camp.
|
||||
It was captured as the baseline (run 366, 499 MB with the world save). The first three restored
|
||||
wipes (367-369) brought back the groups, permissions and members, but not the NPC profiles or the
|
||||
chat titles (DM26). **Public URL:** `demo.runicgateway.com` is served through Pangolin. Cloudflare's
|
||||
proxy in front of it made the app log Cloudflare's addresses as the visitors' IPs, under the
|
||||
default `TRUST_PROXY=1`. Cloudflare was set to DNS only, and the app now logs the real visitor.
|
||||
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