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

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
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,
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
@@ -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) |
| 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) |
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 +532,10 @@ 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.
6. **Play**: the sign-in details in the Play Console point at `play-review`;
runicgateway.com `/beta` publishes the logins.