docs(website): DM25 data: URI lock on the demo + first-wipe notes

DEMO_MODE.md §3.3 gets the data: URI rule (a pasted image is an upload),
§9.1 records DM25, and §10 step 5 records the first wipes: the rig half
worked, and the seed loaded no module because the runner's chown of
modules/ and logs/ failed silently. BACKEND_DESIGN.md §5.1 mirrors the rule.

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-27 13:13:54 -05:00
parent 5628bf131b
commit 8e5d2f5f4e
2 changed files with 24 additions and 1 deletions

View File

@@ -1834,7 +1834,10 @@ it.
A sentence back refuses the write `403 { code: "DEMO_LOCKED", message, errors }` with that sentence; a A sentence back refuses the write `403 { code: "DEMO_LOCKED", message, errors }` with that sentence; a
hook that throws refuses too. hook that throws refuses too.
- **Uploads.** Any request with a `multipart/*` content type is refused app-wide, before a router or - **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 - **Search engines.** Every response carries `X-Robots-Tag: noindex, nofollow`. `/robots.txt` is
`Disallow: /`, served ahead of the SPA's static file. `Disallow: /`, served ahead of the SPA's static file.
- **`GET /public/settings`** gains `demo: { resetsAt, logins }`, from the `demo_state` settings row. - **`GET /public/settings`** gains `demo: { resetsAt, logins }`, from the `demo_state` settings row.

View File

@@ -151,6 +151,20 @@ next wipe.
body is `multipart/form-data`, before a router sees it. An upload route added later is locked 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. 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 `&#100;ata:` 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, **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, 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 respawn), economics and permission grants to a linked player, the world actions events run, and map
@@ -456,6 +470,7 @@ 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) | | 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` | | 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 | | 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) |
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is 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 setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
@@ -517,5 +532,10 @@ through that proxy.
module at 0.6.0) ran locally against an image built from `main`; `rig.mjs plan` and `smoke` ran 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 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. 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.
6. **Play**: the sign-in details in the Play Console point at `play-review`; 6. **Play**: the sign-in details in the Play Console point at `play-review`;
runicgateway.com `/beta` publishes the logins. runicgateway.com `/beta` publishes the logins.