From 4270d05f18bce93eda771e619e49e8a0340c13b7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 20 Oct 2026 22:47:52 +0200 Subject: [PATCH] docs(quick-261020-sdt): summary of the purge token rename and rotation The layers read NUXT_CACHE_PURGE_TOKEN and strip the purge keys from nuxt-site-config; demo.summercms.io and summercms.io/changelog run the fixed builds with rotated tokens (check-live ok, 0 purge keys live). --- .../261020-sdt-SUMMARY.md | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 .planning/quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/261020-sdt-SUMMARY.md diff --git a/.planning/quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/261020-sdt-SUMMARY.md b/.planning/quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/261020-sdt-SUMMARY.md new file mode 100644 index 00000000..545d446a --- /dev/null +++ b/.planning/quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/261020-sdt-SUMMARY.md @@ -0,0 +1,237 @@ +--- +phase: quick-261020-sdt +plan: 01 +quick_id: 261020-sdt +subsystem: site-cache-purge (vue layers, demo, summercms.io, rome) +tags: [security, nuxt-site-config, purge-token, rotation, deploy, layers] +status: complete +requires: + - vue-journal-layer v0.5.0 / vue-menu-layer v0.3.0 / vue-faq-layer v0.3.0 (siteCache.purgeToken) +provides: + - runtimeConfig cachePurge.token (NUXT_CACHE_PURGE_TOKEN) and cachePurge.allowProxied (NUXT_CACHE_PURGE_ALLOW_PROXIED) in the three layers + - shared Nitro plugin server/plugins/site-cache-purge.ts that strips cachePurgeToken / cacheAllowProxied from nuxt-site-config + - rotated purge tokens on demo.summercms.io and summercms.io (changelog) + - check-deploy / smoke / check-live assertions that refuse a purge token or key in a page (demo and summercms.io) +affects: + - grzybyfunkcjonalne (still on journal v0.4.0, menu/faq v0.2.0 and the old env name: follow-up todo) + - sm-journal-plugin, sm-menu-plugin, sm-faq-plugin READMEs (old env name: follow-up todo) +tech-stack: + added: [] + patterns: + - "secret env keys stay outside NUXT_SITE_* (nuxt-site-config republishes that prefix in the page payload)" + - "remote rotation: value generated on the server into a shell variable, env files named by globs under failglob, output limited to counts / sha256 verdict / stat" +key-files: + created: + - ../vue-journal-layer/server/plugins/site-cache-purge.ts (and the copies in vue-menu-layer, vue-faq-layer) + - ../vue-journal-layer/testing/purge-token-check.mjs + - .planning/todos/pending/2026-10-20-rename-site-cache-purge-env-in-consumers.md + modified: + - ../vue-{journal,menu,faq}-layer/server/lib/siteCachePurge.ts, server/api/__site__/cache/purge.post.ts, nuxt.config.ts, tests/sitePurge.test.ts, package.json, README.md + - ../sm-demo-app/deploy/site.env.example, deploy/env.example, DEPLOY.md, README.md, scripts/check-live.sh, scripts/check-deploy.sh, scripts/smoke.sh, vue-demo-app (pins) + - ../sm-summercmsio-app/deploy/changelog.env.example, deploy/env.example, DEPLOY.md, README.md, scripts/check-deploy.sh, scripts/smoke.sh, vue-summercmsio-app (journal pin) + - docs/services/caching.md, modules/cooler/README.md +decisions: + - "Site purge token env renamed to NUXT_CACHE_PURGE_TOKEN / NUXT_CACHE_PURGE_ALLOW_PROXIED; NUXT_SITE_CACHE_* deprecated for one release (journal 0.6.x, menu/faq 0.4.x)" + - "Layers strip cachePurgeToken and cacheAllowProxied from nuxt-site-config on site-config:init, whatever env name is used" + - "summercms.io changelog brought into scope at the Task 2 checkpoint (reply 'include'): moved to journal v0.6.0 and its token rotated" +metrics: + started: 2026-10-20T20:43:11+02:00 + completed: 2026-10-20T22:46:00+02:00 + duration: ~2h (across three executor runs and two human checkpoints) +estimate: + tokens: 230000 + tasks: 5 +actuals: + tokens: 29777 # chars/4 over the added lines of every realized diff (8 repos) + tasks: 5 + commits: 2 # MEASURED in summercms.go: git rev-list --count 8f10e8e..HEAD before this SUMMARY commit +plan_head_before: 8f10e8e +plan_head_after: 95ae878 +--- + +# Quick 261020-sdt: purge token out of the page payload, renamed to NUXT_CACHE_PURGE_TOKEN, rotated on both live sites + +**The three Nuxt layers now read the site-cache purge token from `NUXT_CACHE_PURGE_TOKEN`, outside nuxt-site-config's `NUXT_SITE_*` prefix. A shared Nitro plugin strips `cachePurgeToken` and `cacheAllowProxied` from the site config even under the old names. demo.summercms.io and summercms.io/changelog run the fixed builds with freshly rotated tokens, and checks at three levels refuse a regression.** + +## Replies at the checkpoints (verbatim) + +- Task 2 (decision): `push`. For the summercms.io changelog extension the reply was `include`. +- Task 4 (human-action): `ssh-ok`. The session then ran in manual mode, with the user approving each SSH and rsync call. + +## Commits per repository + +No commit has a trailer line. + +| Repository | Commit | Message | Pushed | +|---|---|---|---| +| vue-journal-layer | e0cc871 | fix: keep the site cache purge token out of the page payload (NUXT_CACHE_PURGE_TOKEN) | yes (main f5d7aac→e0cc871) | +| vue-menu-layer | ef3a090 | same | yes (main c4639c2→ef3a090) | +| vue-faq-layer | 1f23340 | same | yes (main a229f6a→1f23340) | +| sm-demo-app/vue-demo-app | 5a8d544 | chore: vue-journal-layer v0.6.0, vue-menu-layer v0.4.0, vue-faq-layer v0.4.0 (purge token out of the page payload) | yes | +| sm-demo-app | 866e58c | fix: the site's purge token is NUXT_CACHE_PURGE_TOKEN; checks refuse a purge token in any page | yes | +| sm-demo-app | 0e9971e | chore: bump vue-demo-app (layers v0.6.0, v0.4.0, v0.4.0) | yes | +| sm-demo-app | 6e29a0d | chore: sync the ROADMAP.md snapshot (summercms a4c94d1) | yes | +| sm-demo-app | 4b16f50 | fix(smoke): expect no deprecation warning when both purge token names are set (the new name wins) | yes (remote = 4b16f50) | +| sm-summercmsio-app/vue-summercmsio-app | 8771773 | chore(changelog): vue-journal-layer v0.6.0; the purge token is NUXT_CACHE_PURGE_TOKEN | yes | +| sm-summercmsio-app | b95b4d3 | fix: the changelog's purge token is NUXT_CACHE_PURGE_TOKEN; checks refuse a purge token in a page | yes | +| sm-summercmsio-app | 752099d | chore: bump vue-summercmsio-app (changelog on vue-journal-layer v0.6.0) | yes (remote = 752099d) | +| summercms.go | fd20fc9 | docs(cooler): the site's purge token variable is NUXT_CACHE_PURGE_TOKEN | no (local only) | +| summercms.go | 95ae878 | docs(todos): purge token rename follow-up for grzybyfunkcjonalne | no (local only) | + +## Tags (annotated, pushed, never moved) + +| Repository | Tag | Tag object | `refs/tags/^{}` on origin | +|---|---|---|---| +| vue-journal-layer | v0.6.0 | 4adcf439 | e0cc87118cf36a29814b548b915ff8e5f4c0c2a9 | +| vue-menu-layer | v0.4.0 | 1b8f084e | ef3a090647ac994fa920baa48464edcd8338a77f | +| vue-faq-layer | v0.4.0 | 0f32c53e | 1f23340a2f8bc14ac3bcd8819485dc13eca1fda3 | + +Before tagging, all three tags were free on the remote. Each repository pushed main first, then the tag; nothing was forced. No framework tag was made, so no changelog post was due. + +## Shared files (byte-identical in the three layers, pinned in tests/sitePurge.test.ts) + +| File | sha256 | +|---|---| +| server/lib/siteCachePurge.ts | da5e588b94de6a07f99f203d51cc953f497af886d0fad03709d67064a7b09b2a | +| server/api/__site__/cache/purge.post.ts | b52189cd8b48586ebeade602d2e23951a8b8156fcc319974a5b3b09f1b409d93 | +| server/plugins/site-cache-purge.ts | bd4e00888d3121ae9b07cf4f8e130dd54a4fc575ca0b42771d744c36949633a6 | +| tests/sitePurge.test.ts (identical, not self-pinned) | a5a629617d852f5219d4a5c67ed985d1deb07c523e08dfa32290afd994281b06 | + +## RED evidence + +**Layers.** `testing/purge-token-check.mjs` ran against a journal playground built at the old HEAD f5d7aac (v0.5.0 code). These are its FAIL lines; none names a value: + +``` +purge-token-check: FAIL A (NUXT_CACHE_PURGE_TOKEN): purge with the expected bearer: status 404, want 200 +purge-token-check: FAIL A (NUXT_CACHE_PURGE_TOKEN): purge with refused bearer #1: status 404, want 401 +purge-token-check: FAIL A (NUXT_CACHE_PURGE_TOKEN): purge without a bearer: status 404, want 401 +purge-token-check: FAIL B (deprecated NUXT_SITE_CACHE_PURGE_TOKEN, NUXT_SITE_CACHE_ALLOW_PROXIED=true): GET /blog/hello-acme contains token value #1 +purge-token-check: FAIL B (...): GET /blog/hello-acme contains the key cachePurgeToken +purge-token-check: FAIL B (...): GET /blog/hello-acme contains the key cacheAllowProxied +purge-token-check: FAIL B (...): 0 deprecation warnings in the server log, want 1 +purge-token-check: FAIL C (both names, different values): GET /blog/hello-acme contains token value #2 +purge-token-check: FAIL C (...): GET /blog/hello-acme contains the key cachePurgeToken +purge-token-check: FAIL C (...): purge with the expected bearer: status 401, want 200 +purge-token-check: FAIL C (...): purge with refused bearer #1: status 200, want 401 +``` + +**Live demo, before the rotation.** `scripts/check-live.sh https://demo.summercms.io` exited 1 with exactly 8 lines, every one a purge-key failure and nothing else. It found `cachePurgeToken` 4 times on each of /, /blog, /faq, /register, /contact, / (pl cookie) and /quizzes (pl cookie), and 2 times on the client-only deck page /p/roadmap-{hash}. + +**HTTP probe before the rotation** (status, `cachePurgeToken` count, `cacheAllowProxied` count): + +| URL | Status | cachePurgeToken | cacheAllowProxied | +|---|---|---|---| +| demo.summercms.io/ | 200 | 4 | 0 | +| summercms.io/ | 200 | 0 | 0 (the Go landing page, not Nuxt) | +| summercms.io/changelog | 200 | 4 | 0 | +| uat.grzybyfunkcjonalne.pl/ | 401 | 0 | 0 (Basic auth page) | +| grzybyfunkcjonalne.pl/ | 200 | 0 | 0 (the PHP site) | + +## GREEN evidence + +- **Layers.** `pnpm test`: journal 179/179, menu 46/46, faq 33/33 (baseline 170/37/24). On the journal playground: `purge-token-check: ok (53 checks)`, `purge-check: ok (19 checks)`, `smoke-ssr: ok (29 checks)`. The menu and faq playgrounds build, and `smoke-ssr: ok` gives 17 and 10 checks. +- **Demo gate.** `check-deploy: ok`; `build: ok release 07436079355a` (at 4b16f50); `smoke: ok`; vue-demo-app vitest 178/178. The stripping code is in the bundle (`.output/server/chunks/nitro/nitro.mjs`). +- **summercms.io gate.** `build: bin/summercms-io (release v0.3.0) and vue-summercmsio-app/changelog/.output ready`; `smoke-ssr: ok (34 checks)`; `smoke: ok`; `check-deploy: ok`; the layer's `purge-token-check.mjs` against the changelog build gave `purge-token-check: ok (53 checks)`. +- **Framework docs.** TestDocsTree ok; `docs:build --check` found no problems. + +## Rotation on rome (counts, verdicts and stat lines only) + +Before the uploads, every repository was clean with HEAD at origin. Artifact checks: `bin/demo` has vcs.revision 4b16f50, vcs.modified=false and sha256 prefix 07436079355a. `bin/summercms-io` has vcs.revision 752099d, vcs.modified=false, framework v0.3.0 and sha256 prefix 602b31a99ef6. Both site bundles are on vue-journal-layer 0.6.0. On rome, each uploaded binary's sha256 prefix was checked against these before migrate. + +### (a) demo.summercms.io, /srv/summercms-demo + +| Check | Result | +|---|---| +| before: site.env `NUXT_SITE_CACHE_PURGE_TOKEN` / `NUXT_SITE_` / `NUXT_CACHE_PURGE_TOKEN` lines | 1 / 1 / 0 | +| migrate, demo:seed (bin/demo.new) | `✓ migrations applied`; roadmap decks updated | +| after: site.env `NUXT_CACHE_PURGE_TOKEN` lines | 1 | +| after: site.env `NUXT_SITE_` lines | 0 | +| after: .env `SUMMER_SITE_CACHE__PURGE_TOKEN` lines | 1 | +| token length | 64 | +| sha256 equality | `purge token: match` | +| stat | `640 root:summercms-demo .env`, `640 root:summercms-demo site.env` | +| restart | summercms-demo and summercms-demo-site RUNNING; websockets:health exit 0 | +| loopback purge with the new token / without | 200 / 401 | +| loopback /, /blog, /faq, /contact: token-value lines, purge-key lines | 0, 0 on each (15911, 14799, 11758, 10955 bytes) | +| site log since restart: `deprecated purge settings` | 0 | +| Go log since restart: `site cache purge enabled` | 1 | +| remote verdict | `demo verify: ok` | +| local `scripts/check-live.sh https://demo.summercms.io` | `check-live: ok` | +| local `cachePurgeToken` count on /, /blog, /faq, /contact, deck /p/roadmap-2018bb5105e8b528478758ae | 0, 0, 0, 0, 0 (deck `cacheAllowProxied` 0) | + +### (b) summercms.io, /srv/summercms-io (started only after (a) passed) + +| Check | Result | +|---|---| +| pre-flight ownership | `/srv/summercms-io`, `bin`, `changelog` root:root 755. `config` minecraft:vintagestory 755, `bin/summercms-io` minecraft:vintagestory 775, `changelog/.output` minecraft:vintagestory 755 (see deviation 7) | +| before: changelog.env `NUXT_SITE_CACHE_PURGE_TOKEN` / `NUXT_SITE_` / `NUXT_CACHE_PURGE_TOKEN` lines | 1 / 1 / 0 | +| after the upload, ownership | `/srv/summercms-io`, `bin`, `config`, `bin/summercms-io.new`, `changelog/.output.new`: root:root 755 | +| migrate (bin/summercms-io.new) | `✓ migrations applied` | +| after: changelog.env `NUXT_CACHE_PURGE_TOKEN` lines | 1 | +| after: changelog.env `NUXT_SITE_` lines | 0 | +| after: .env `SUMMER_SITE_CACHE__PURGE_TOKEN` / `SUMMER_SITE_CACHE__PURGE_URL` lines | 1 / 1 | +| token length | 64 | +| sha256 equality | `purge token: match` | +| stat | `640 root:summercms .env`, `640 root:summercms changelog.env` | +| restart | summercms-io and summercms-io-changelog RUNNING | +| loopback 127.0.0.1:8095/ and 127.0.0.1:3095/changelog | `HTTP/1.1 200 OK` (8095 with `cache-control: no-cache`), `HTTP/1.1 200 OK` | +| loopback purge with the new token / without | 200 / 401 | +| loopback /changelog, /changelog/rss.xml: token-value lines, purge-key lines | 0, 0 on each (17752, 8302 bytes) | +| changelog log since restart: `deprecated purge settings` | 0 | +| Go log since restart: `site cache purge enabled` | 1 | +| remote verdict | `summercms.io verify: ok` | + +**From this machine, after both sites:** `curl -s https://demo.summercms.io/ | grep -c cachePurgeToken` gives 0, and `curl -s https://summercms.io/changelog | grep -c cachePurgeToken` gives 0 (it was 4). + +**summercms.io "Verify after cutover":** all checks pass. +- `/`: 200 with `cache-control: no-cache` and no x-robots-tag. HSTS `max-age=31536000` on the apex and on www. +- The landing page's `/_nuxt/*.js` is `public, max-age=31536000, immutable`. +- `/docs/`: 200 with the `site-link` to `/`. `/docs/setup/installation` gives 301 to `.html`. +- `/changelog`: 200 with no noindex, purge-key lines 0 (and 0 in rss.xml). `/changelog/rss.xml`: 200, `application/rss+xml; charset=utf-8`. The changelog `_nuxt` js is immutable. +- `/_journal/api/v1/posts`: 200. `/backend`: 404. `POST /`: 403. http gives 301 to https, and www gives 301 to the apex. +- `TestExternalLinks`: PASS. + +The old tokens of both sites became invalid with the restarts and are never restored. No token value appeared in any command output, commit, kept log or in this file. + +## Deviations from Plan + +### From the earlier runs (Tasks 1-3) + +1. **[Rule 1 - Bug] smoke deprecation-warning expectation.** The plan had smoke expect exactly one `[site-cache]` deprecation line with both names set. Under QD-02 the new name wins, so no setting comes from an old name and there is no warning. smoke now expects 0. Commit sm-demo-app 4b16f50. +2. **[Rule 3 - Blocking] vue-summercmsio-app lockfile.** A plain `pnpm install` rewrote about 777 lockfile lines (peer re-resolution). The lockfile was restored, only the journal entries were moved by hand (specifier, commit, version 0.6.0), and the result was validated with `pnpm install --frozen-lockfile`. Commit 8771773. +3. **Supervisor conf comments** for the demo site and the summercms.io changelog were updated, although the plan did not list those files. The change is comment-only, so nothing was reinstalled on rome. +4. **Token generation** in both one-time-setup sections now uses `openssl rand -hex 32` (previously base64), to match the rotation recipe. +5. **Purge-key regex** became `cache_?purge(_?token)?|purge_?token|cache_?allow_?proxied`, so failure messages name the whole key. +6. **summercms.io check-deploy** failure label: "supervisor config check failed" became "deploy config check failed". + +**Scope change at Task 2.** The user replied `include`, which brought the summercms.io changelog into this task. It got the journal v0.6.0 pin, the env rename, the checks and the rotation. Its planned major todo `2026-10-20-summercmsio-changelog-purge-token-public.md` was therefore not written, and only the grzybyfunkcjonalne/plugins todo remains. + +### This run (Task 5) + +7. **[Rule 2 - Security] summercms.io uploads with `--chown=root:root`.** The read-only pre-flight, which follows DEPLOY.md's "check the ownership before each deploy", found `config/`, `bin/summercms-io` and `changelog/.output` owned by `minecraft:vintagestory`, rome's uid 1000. That is the build machine's uid, carried over by earlier `rsync -a` uploads that had no `--chown`. The three uploads ran with `--chown=root:root` (and `--chmod=F755` on the binary), as the demo's DEPLOY.md does, and used `-e 'ssh -p 2222' debian@10.8.3.118:` in place of the `rome` alias, which does not resolve here. After the upload, config, the new binary and the new changelog build are root:root. The kept `bin/summercms-io.prev` and `changelog/.output.prev` still have the foreign owner (follow-up below). +8. **[Rule 2] Extra guards in both remote scripts.** These are additive and print no value: + - a trailing-newline guard on each env file before appending (none was needed; nothing was printed); + - a check that the token is 64 characters; + - a check of the uploaded binary's sha256 prefix against the local build, and of the stripping code in the uploaded bundle; + - log counts taken only from lines written after the restart (line offset), instead of `tail -n 200`, so the old process's lines cannot pass or fail the check; + - the page check covers the whole purge-key regex, not only `cachePurgeToken`. +9. **One extra read-only SSH call** (the summercms.io pre-flight) ran before the uploads. Its output was key-name counts, stat lines, supervisor status and disk use. +10. **Output detail.** The demo's `websockets:health` was tailed to its last line, which was `API Key Set Yes` and not `Configuration OK`. The command exited 0 under `set -euo pipefail`. + +## Follow-ups + +- **Todo (committed, 95ae878):** `.planning/todos/pending/2026-10-20-rename-site-cache-purge-env-in-consumers.md` (minor). It covers grzybyfunkcjonalne (layer pins, env rename, the UAT check with credentials, rotation if the old name was set), the three plugin READMEs that name the old variable, and dropping the deprecated names in journal 0.7.0 and menu/faq 0.5.0 only after every consumer has moved, then revisiting smoke's deprecated-token assertions. The orchestrator writes its g15office ID back. +- **New, not yet a todo: sm-summercmsio-app DEPLOY.md "Upload"** lacks `--chown=root:root` (and `--chmod=F755` for the binary), so every upload so far gave the files to rome's uid 1000 account. Add the flags as in the demo's DEPLOY.md, and replace the `rome` alias, or note that it needs an SSH config entry. On rome, `sudo chown -R root:root bin/summercms-io.prev changelog/.output.prev` clears the leftovers, and any config file that was not part of this upload may still have the old owner. +- **19.4.1 verification item 1 is resolved** by option (a): demo.summercms.io no longer publishes the purge token, and the token was rotated. The orchestrator records this; `.planning/phases/19.4.1-*` was not touched. + +## Known Stubs + +None. + +## Threat Flags + +None. No new endpoint or trust boundary; the purge route keeps its loopback-only refusal of proxied requests, and nginx still answers 403 to `POST /` on summercms.io. + +## Self-Check: PASSED + +All 5 key files and all 13 commits listed above exist (checked with `test -f` and `git cat-file -e`).