From 8e5d2f5f4edab9466428b69c4c0430c350a11059 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 27 Oct 2026 13:13:54 -0500 Subject: [PATCH] docs(website): DM25 data: URI lock on the demo + first-wipe notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- website/BACKEND_DESIGN.md | 5 ++++- website/DEMO_MODE.md | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index fc90fa5..8ed257f 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -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. diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index 2646ab5..910b57b 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -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 `` 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.