docs(quick-261020-sdt): plan purge token rename and rotation
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
---
|
||||
phase: quick-261020-sdt
|
||||
plan: 01
|
||||
quick_id: 261020-sdt
|
||||
slug: rotate-the-purge-token-and-rename-the-la
|
||||
type: execute
|
||||
mode: quick
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- ../vue-journal-layer/server/lib/siteCachePurge.ts
|
||||
- ../vue-journal-layer/server/api/__site__/cache/purge.post.ts
|
||||
- ../vue-journal-layer/server/plugins/site-cache-purge.ts
|
||||
- ../vue-journal-layer/nuxt.config.ts
|
||||
- ../vue-journal-layer/tests/sitePurge.test.ts
|
||||
- ../vue-journal-layer/testing/purge-token-check.mjs
|
||||
- ../vue-journal-layer/testing/nuxt-server.mjs
|
||||
- ../vue-journal-layer/package.json
|
||||
- ../vue-journal-layer/README.md
|
||||
- ../vue-menu-layer/server/lib/siteCachePurge.ts
|
||||
- ../vue-menu-layer/server/api/__site__/cache/purge.post.ts
|
||||
- ../vue-menu-layer/server/plugins/site-cache-purge.ts
|
||||
- ../vue-menu-layer/nuxt.config.ts
|
||||
- ../vue-menu-layer/tests/sitePurge.test.ts
|
||||
- ../vue-menu-layer/package.json
|
||||
- ../vue-menu-layer/README.md
|
||||
- ../vue-faq-layer/server/lib/siteCachePurge.ts
|
||||
- ../vue-faq-layer/server/api/__site__/cache/purge.post.ts
|
||||
- ../vue-faq-layer/server/plugins/site-cache-purge.ts
|
||||
- ../vue-faq-layer/nuxt.config.ts
|
||||
- ../vue-faq-layer/tests/sitePurge.test.ts
|
||||
- ../vue-faq-layer/package.json
|
||||
- ../vue-faq-layer/README.md
|
||||
- ../sm-demo-app/vue-demo-app/package.json
|
||||
- ../sm-demo-app/vue-demo-app/pnpm-lock.yaml
|
||||
- ../sm-demo-app/vue-demo-app/README.md
|
||||
- ../sm-demo-app/vue-demo-app (submodule pointer in sm-demo-app)
|
||||
- ../sm-demo-app/deploy/site.env.example
|
||||
- ../sm-demo-app/deploy/env.example
|
||||
- ../sm-demo-app/DEPLOY.md
|
||||
- ../sm-demo-app/README.md
|
||||
- ../sm-demo-app/scripts/check-live.sh
|
||||
- ../sm-demo-app/scripts/check-deploy.sh
|
||||
- ../sm-demo-app/scripts/smoke.sh
|
||||
- docs/services/caching.md
|
||||
- modules/cooler/README.md
|
||||
- .planning/todos/pending/2026-10-20-summercmsio-changelog-purge-token-public.md
|
||||
- .planning/todos/pending/2026-10-20-rename-site-cache-purge-env-in-consumers.md
|
||||
autonomous: false
|
||||
requirements: [QUICK-261020-sdt]
|
||||
source:
|
||||
- orchestrator brief for quick task 261020-sdt (goal 1-4, constraints)
|
||||
- .planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-VERIFICATION.md lines 30-32, 211, 221-224 (the finding; read only, never edit)
|
||||
- ../vue-journal-layer/nuxt.config.ts (runtimeConfig siteCache.purgeToken / allowProxied, NUXT_SITE_CACHE_* comments), server/lib/siteCachePurge.ts, server/api/__site__/cache/purge.post.ts, tests/sitePurge.test.ts (SHARED_LIB_SHA256 0ea9f0f9..., SHARED_ROUTE_SHA256 8f862260..., callRoute stub at lines 142-181), testing/nuxt-server.mjs (startNuxtServer captures child output in `log` but does not return it), testing/purge-check.mjs, README.md lines 105-145 and 196-208
|
||||
- ../vue-menu-layer and ../vue-faq-layer nuxt.config.ts, README "Configuration" and "Site cache purge" (same NUXT_SITE_CACHE_* names); the two shared files and tests/sitePurge.test.ts are byte-identical across the three layers today (checked with sha256sum and diff)
|
||||
- nuxt-site-config 4.2.3 (through @nuxtjs/seo 5.2.1 in vue-journal-layer only) - site-config-stack envSiteConfig maps every NUXT_SITE_* and NUXT_PUBLIC_SITE_* variable to a camelCase site-config key (NUXT_SITE_CACHE_PURGE_TOKEN -> cachePurgeToken, NUXT_SITE_CACHE_ALLOW_PROXIED -> cacheAllowProxied); runtime/server/middleware/init.js pushes `...serverEnvSiteConfig` (from import.meta.env) into the per-request stack and then calls the Nitro hook `site-config:init` with ctx { siteConfig, event }; runtime/app/plugins/0.siteConfig.js serializes the stack into useState('site-config') (page payload), runtime/server/plugins/injectState.js writes window.__NUXT_SITE_CONFIG__ on ssr:false pages
|
||||
- ../sm-demo-app DEPLOY.md (One-time server setup lines 117-143, Upload 341-350, Release 352-390, Verify 414-444, Rollback 446-457, Secrets 484-514), deploy/site.env.example, deploy/env.example, scripts/check-live.sh (get, no_admin_link helpers), scripts/check-deploy.sh (Python block reading site_env at ~line 232), scripts/smoke.sh (Nuxt started with env -i at ~line 179, page loop ~line 205), README.md Release section
|
||||
- ../sm-demo-app/vue-demo-app package.json (layers pinned at vue-journal-layer #v0.5.0, vue-menu-layer #v0.3.0, vue-faq-layer #v0.3.0) and README table; routeRules '/p/**' and '/presentations/**' are ssr:false; no swr route caching
|
||||
- consumer docs read for the follow-up only - ../sm-summercmsio-app (DEPLOY.md lines 118-133, deploy/changelog.env.example, deploy/env.example, vue-summercmsio-app/changelog/nuxt.config.ts line 83, vue-summercmsio-app/README.md line 43, changelog pinned to vue-journal-layer #v0.5.0); ../sm-grzybyfunkcjonalne-app (README lines 122-127, DEPLOY.md lines 85-86 and 211, deploy/nuxt.env.example, vue app pinned to journal #v0.4.0, menu and faq #v0.2.0, nuxt.config.ts comment naming NUXT_JOURNAL_CACHE_PURGE_TOKEN)
|
||||
- framework docs naming the old env names - docs/services/caching.md lines 170-178, modules/cooler/README.md lines 207-208
|
||||
- plugin READMEs naming the old env name (follow-up only) - sm-journal-plugin README "Receiver" line, sm-menu-plugin README line 146, sm-faq-plugin README line 73
|
||||
- HTTP probe at planning time (2026-10-20, counts only, no value printed) - demo.summercms.io/ cachePurgeToken x4; summercms.io/ x0 (that page is served by the Go landing, not by the changelog Nuxt app); summercms.io/changelog x4 (the changelog Nuxt app on vue-journal-layer v0.5.0 with NUXT_SITE_CACHE_PURGE_TOKEN in changelog.env - it leaks too); uat.grzybyfunkcjonalne.pl answers 401 Basic auth to anonymous requests (the verifier saw the auth page, not the site); grzybyfunkcjonalne.pl serves the PHP site (no Nuxt payload)
|
||||
- remote tags at planning time - vue-journal-layer up to v0.5.0, vue-menu-layer and vue-faq-layer up to v0.3.0 (v0.6.0 / v0.4.0 / v0.4.0 are free); pnpm test baseline 170/37/24 pass
|
||||
|
||||
estimate:
|
||||
tokens: 230000
|
||||
raw_tokens: 230000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "QD-01/QD-03: a Nuxt server built from vue-journal-layer 0.6.0 and started with NUXT_CACHE_PURGE_TOKEN, with the deprecated NUXT_SITE_CACHE_PURGE_TOKEN (plus NUXT_SITE_CACHE_ALLOW_PROXIED=true), or with both, serves pages whose HTML (and _payload.json when the build serves one) contains neither token value nor the key names cachePurgeToken and cacheAllowProxied: testing/purge-token-check.mjs ends with 'purge-token-check: ok'."
|
||||
- "QD-02: the shared route POST /api/__site__/cache/purge accepts Bearer <NUXT_CACHE_PURGE_TOKEN>; with only NUXT_SITE_CACHE_PURGE_TOKEN set it accepts that token and the server logs exactly one deprecation warning naming NUXT_CACHE_PURGE_TOKEN (never the value); with both set the new value wins and the old one gets 401; NUXT_CACHE_PURGE_ALLOW_PROXIED=true and the deprecated NUXT_SITE_CACHE_ALLOW_PROXIED=true each let a proxied request reach the token check."
|
||||
- "QD-04: vue-journal-layer, vue-menu-layer and vue-faq-layer ship server/lib/siteCachePurge.ts, server/api/__site__/cache/purge.post.ts, server/plugins/site-cache-purge.ts and tests/sitePurge.test.ts byte for byte; the test pins the three shared files' sha256; pnpm test passes in all three and the menu and faq playgrounds build and pass smoke:ssr (the plugin is inert without nuxt-site-config)."
|
||||
- "QD-05: annotated tags vue-journal-layer v0.6.0, vue-menu-layer v0.4.0 and vue-faq-layer v0.4.0 exist on git.golem15.com only after the user answered 'push' at the decision checkpoint; each layer README documents the new names, the one-release deprecation of the NUXT_SITE_CACHE_* names, the site-config stripping and the advice to rotate a token ever set under the old name."
|
||||
- "QD-06: sm-demo-app/vue-demo-app pins the three new tags; deploy/site.env.example declares NUXT_CACHE_PURGE_TOKEN and no NUXT_SITE_ key; scripts/check-deploy.sh fails on a NUXT_SITE_ key in site.env.example; scripts/smoke.sh fails when a server-rendered or client-only page carries a purge token value or key name, and checks that the new token purges (200) while the deprecated one set alongside gets 401; scripts/check-live.sh fails on a purge key name in any page it fetches (it failed against the leaking live site before the rotation)."
|
||||
- "QD-07: after the user's go-ahead, demo.summercms.io runs the new build with a rotated token: site.env holds one NUXT_CACHE_PURGE_TOKEN line and no NUXT_SITE_ line, .env's SUMMER_SITE_CACHE__PURGE_TOKEN holds the same value (sha256 verdict 'match'), both files stay 640 root:summercms-demo, a loopback purge with the new token answers 200, no page fetched on rome contains the token value, and check-live prints 'check-live: ok'. No secret value appears in any command output, commit, kept log or SUMMARY."
|
||||
- "QD-08: summercms.io and grzybyfunkcjonalne get no deploy and no env change; two todos record their follow-up (summercms.io/changelog's live exposure first), and the framework docs name NUXT_CACHE_PURGE_TOKEN (committed locally, not pushed)."
|
||||
artifacts:
|
||||
- path: ../vue-journal-layer/server/lib/siteCachePurge.ts
|
||||
provides: "Shared pure helpers: existing authorizeSitePurge, purgeSiteRouteCache, parseSitePurgeEvent plus resolveSitePurgeConfig, SITE_CONFIG_PURGE_KEYS, redactSiteConfigStack, sitePurgeDeprecationMessage, SITE_PURGE_WARNED"
|
||||
- path: ../vue-journal-layer/server/api/__site__/cache/purge.post.ts
|
||||
provides: "The shared route reading its token and allowProxied through resolveSitePurgeConfig"
|
||||
- path: ../vue-journal-layer/server/plugins/site-cache-purge.ts
|
||||
provides: "Shared Nitro plugin: strips the purge keys from nuxt-site-config's stack on site-config:init and logs one deprecation warning per process"
|
||||
- path: ../vue-journal-layer/testing/purge-token-check.mjs
|
||||
provides: "End-to-end check on a built server: no token or purge key in pages under new, deprecated and both env names; purge 200/401/404 per scenario"
|
||||
- path: ../sm-demo-app/scripts/check-live.sh
|
||||
provides: "Live assertion: no purge key name in any page it fetches"
|
||||
- path: ../sm-demo-app/scripts/smoke.sh
|
||||
provides: "Release-build assertion: no purge token value or key in any page; new token purges, deprecated one refused"
|
||||
- path: ../sm-demo-app/scripts/check-deploy.sh
|
||||
provides: "Committed-file assertion: site.env.example has NUXT_CACHE_PURGE_TOKEN and no NUXT_SITE_ key"
|
||||
key_links:
|
||||
- from: "server/api/__site__/cache/purge.post.ts"
|
||||
to: "runtimeConfig.cachePurge.token / runtimeConfig.siteCache.purgeToken"
|
||||
via: "resolveSitePurgeConfig(useRuntimeConfig(event)) then authorizeSitePurge"
|
||||
- from: "server/plugins/site-cache-purge.ts"
|
||||
to: "nuxt-site-config per-request stack"
|
||||
via: "nitroApp.hooks.hook('site-config:init', ctx => redactSiteConfigStack(ctx.siteConfig.stack))"
|
||||
- from: "rome /srv/summercms-demo/site.env NUXT_CACHE_PURGE_TOKEN"
|
||||
to: "rome /srv/summercms-demo/.env SUMMER_SITE_CACHE__PURGE_TOKEN"
|
||||
via: "one value generated on rome, written to both, sha256 equality checked before the restart"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Stop publishing the site-cache purge token in the page payload of every site built on the shared layers, rotate the demo's token on rome, and keep every consumer working.
|
||||
|
||||
Root cause: nuxt-site-config (pulled in by @nuxtjs/seo in vue-journal-layer) copies every `NUXT_SITE_*` environment variable into the public site config, so the documented `NUXT_SITE_CACHE_PURGE_TOKEN` shows up as `cachePurgeToken` in each page's payload (and `NUXT_SITE_CACHE_ALLOW_PROXIED` as `cacheAllowProxied`). The fix in the layers has two parts. The variables get new names outside that prefix (QD-01). A shared Nitro plugin also strips the two keys from the site config, so a site that keeps the old names during the deprecation release no longer leaks either (QD-03). The old names keep working for one release (QD-02). After that the demo moves to the new tags and gets checks that refuse a leaked token (QD-06), and the demo's token is rotated on rome behind a stop before any SSH (QD-07). Consumers get follow-up todos only (QD-08).
|
||||
|
||||
Quick-task decisions (QD) this plan implements:
|
||||
- QD-01: New names: runtimeConfig `cachePurge.token` (`NUXT_CACHE_PURGE_TOKEN`) and `cachePurge.allowProxied` (`NUXT_CACHE_PURGE_ALLOW_PROXIED`). Both old names move, because nuxt-site-config republishes `NUXT_SITE_CACHE_ALLOW_PROXIED` too.
|
||||
- QD-02: Deprecation for one release (journal 0.6.x, menu and faq 0.4.x; removed in the next minor): `siteCache.purgeToken` / `siteCache.allowProxied` (`NUXT_SITE_CACHE_PURGE_TOKEN` / `NUXT_SITE_CACHE_ALLOW_PROXIED`) keep working. A non-empty new token wins over the old one, and allowProxied is on when either flag is `true` or `"true"`. When an old name is in use, the process logs one deprecation warning that names the new variables and says to rotate. The warning never contains a value.
|
||||
- QD-03: Defense in depth: shared plugin `server/plugins/site-cache-purge.ts` hooks nuxt-site-config's `site-config:init` and deletes `cachePurgeToken` and `cacheAllowProxied` from every stack entry. It does nothing in an app without nuxt-site-config, and it is idempotent whether Nitro runs one copy or three.
|
||||
- QD-04: The three shared files (lib, route, plugin) and `tests/sitePurge.test.ts` are byte-identical in the three layers, with three sha256 pins.
|
||||
- QD-05: Versions: vue-journal-layer 0.6.0, vue-menu-layer 0.4.0, vue-faq-layer 0.4.0. These are minor bumps (new config keys plus a deprecation, as v0.5.0 was for allowProxied). package.json moves in the same commit. Annotated tags are pushed only after the blocking decision checkpoint.
|
||||
- QD-06: Demo: the pins move, the env is renamed in site.env.example, env.example, DEPLOY.md and README, and three checks are added: check-deploy (committed files), smoke (release build, known throwaway tokens) and check-live (live pages, key names).
|
||||
- QD-07: Rotation on rome (demo only): the new token comes from `openssl rand -hex 32` into a remote shell variable and is written to both files without being printed. Remote commands name the env files through globs (`.en[v]`, `site.en[v]`) with `shopt -s failglob`. One restart puts the new build and the new value live together. A separate task does this, after a human-action stop that comes before the first SSH or rsync.
|
||||
- QD-08: summercms.io and grzybyfunkcjonalne get no deploy and no env change. Two todos record the follow-up. summercms.io/changelog leaks its production token right now (planning-time probe) and comes first. The framework docs (caching guide, cooler README) are updated to the new name, committed but not pushed.
|
||||
|
||||
Purpose: a published secret is a standing risk (it becomes usable the moment a site sets allowProxied or opens POST), and the fix must not break grzybyfunkcjonalne (production) or summercms.io.
|
||||
Output: three layer commits and tags, demo commits and pushes, a rotated live demo, framework docs commit, two todos, SUMMARY.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@CLAUDE.md
|
||||
@../CLAUDE.md
|
||||
@.planning/G15OFFICE.md
|
||||
@../vue-journal-layer/README.md
|
||||
@../vue-journal-layer/nuxt.config.ts
|
||||
@../vue-journal-layer/server/lib/siteCachePurge.ts
|
||||
@../vue-journal-layer/server/api/__site__/cache/purge.post.ts
|
||||
@../vue-journal-layer/tests/sitePurge.test.ts
|
||||
@../vue-journal-layer/testing/nuxt-server.mjs
|
||||
@../vue-journal-layer/testing/purge-check.mjs
|
||||
@../sm-demo-app/DEPLOY.md
|
||||
@../sm-demo-app/deploy/site.env.example
|
||||
@../sm-demo-app/deploy/env.example
|
||||
@../sm-demo-app/scripts/check-live.sh
|
||||
@../sm-demo-app/scripts/check-deploy.sh
|
||||
@../sm-demo-app/scripts/smoke.sh
|
||||
|
||||
Absolute root of the sibling repositories: /media/nvme/dev/golem15/summercms.io/summercms (below: S). The working directory is S/summercms.go. The demo's site is the submodule S/sm-demo-app/vue-demo-app (remote golem15/vue-demo-app). The sibling directory S/vue-demo-app is a stale local copy with no remote; never edit it.
|
||||
|
||||
Rules for every task (from CLAUDE.md, the user's global rules and the brief):
|
||||
- Never read or print secret values. A hook blocks commands that name the env file: use globs (`.en[v]`, `site.en[v]`) and key-name greps (`grep -c '^KEY='`) only. Never echo a token, never put one on a command line (curl reads the header from a process substitution), and never write one into a commit, log excerpt or SUMMARY.
|
||||
- Commits: one logical change per commit, staged by explicit path (never `git add -A` or `git add .`), and no `Co-Authored-By` or any other trailer line. This overrides the harness attribution reminder. Never force-push. Never push or tag summercms.go (the framework) or sm-bm-app.
|
||||
- Other sessions work in summercms.go: never touch `.planning/STATE.md`, `.planning/state.json` or `.planning/phases/19.4.1-*`. Before editing docs/services/caching.md or modules/cooler/README.md, check `git status --short` on them and leave them alone if another session has uncommitted edits (record that in the SUMMARY). The orchestrator updates STATE through `msd_run query state.*`, creates the g15office tasks for the two new todos (and writes their IDs back), and posts the g15office comment on the 19.4.1 task g02hcroj. The executor does none of these.
|
||||
- No new npm dependency. `pnpm install` in vue-demo-app may only re-resolve the three golem15 layers.
|
||||
- The layers are shared core code: nothing a consumer relies on today may stop working (the old env names, the old `/api/__journal__/cache/purge` alias route, `authorizeSitePurge` and the other exports, startNuxtServer's existing return fields).
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Layers - the purge token leaves the NUXT_SITE_ prefix and the site config in vue-journal-layer, vue-menu-layer and vue-faq-layer (local commits, no tag)</name>
|
||||
<files>../vue-journal-layer/server/lib/siteCachePurge.ts, ../vue-journal-layer/server/api/__site__/cache/purge.post.ts, ../vue-journal-layer/server/plugins/site-cache-purge.ts, ../vue-journal-layer/nuxt.config.ts, ../vue-journal-layer/tests/sitePurge.test.ts, ../vue-journal-layer/testing/purge-token-check.mjs, ../vue-journal-layer/testing/nuxt-server.mjs, ../vue-journal-layer/package.json, ../vue-journal-layer/README.md, and in ../vue-menu-layer and ../vue-faq-layer: server/lib/siteCachePurge.ts, server/api/__site__/cache/purge.post.ts, server/plugins/site-cache-purge.ts, nuxt.config.ts, tests/sitePurge.test.ts, package.json, README.md</files>
|
||||
<behavior>
|
||||
- resolveSitePurgeConfig({ cachePurge: { token: A }, siteCache: { purgeToken: B } }) returns token A and an empty deprecated list (QD-02: new wins).
|
||||
- resolveSitePurgeConfig({ cachePurge: { token: '' }, siteCache: { purgeToken: B } }) returns token B with 'token' in deprecated; with cachePurge missing entirely, the same.
|
||||
- allowProxied is true for cachePurge.allowProxied true or 'true', or for siteCache.allowProxied true or 'true' (then 'allowProxied' is in deprecated). It is false for false, undefined, 'false' and 1. resolveSitePurgeConfig(undefined) returns token undefined, allowProxied false and no deprecated entries, without throwing.
|
||||
- The route answers 200 for Bearer TOKEN with { cachePurge: { token: TOKEN } }, and also with only { siteCache: { purgeToken: TOKEN } }. With both set to different values, the new one gets 200 and the old one 401. With cachePurge.allowProxied true, a proxied request reaches the token check. Every existing sitePurge route spec still passes.
|
||||
- redactSiteConfigStack on [{ url, name, cachePurgeToken, cacheAllowProxied }, { _context: 'x' }] removes the two keys, keeps url, name and _context, and returns 2. A non-array, undefined, or entries that are not objects return 0 without throwing (QD-03).
|
||||
- The plugin registers a 'site-config:init' hook on a fake nitroApp, and calling the hook with { siteConfig: { stack } } strips the keys. With siteCache.purgeToken set, it calls console.warn once, and the text names NUXT_CACHE_PURGE_TOKEN and NUXT_SITE_CACHE_PURGE_TOKEN but not the value. A second plugin invocation, standing in for another layer's copy, does not warn again (globalThis[SITE_PURGE_WARNED]). With only cachePurge.token set it never warns.
|
||||
- sha256 pins: SHARED_LIB_SHA256, SHARED_ROUTE_SHA256 and the new SHARED_PLUGIN_SHA256 match the files (QD-04).
|
||||
- E2E (journal only), testing/purge-token-check.mjs against the built playground, three scenarios: (A) the new name only, (B) the deprecated names only, with allowProxied true, (C) both names with different values. In each, no page body or served _payload.json contains a token value, cachePurgeToken or cacheAllowProxied. The purge answers 200 for the expected bearer and 401 for the other or a wrong one. In B a proxied POST with the bearer answers 200, and in A one answers 404. B's server output contains the deprecation warning once and A's has none. The script ends 'purge-token-check: ok (n checks)'.
|
||||
</behavior>
|
||||
<action>
|
||||
Work in S/vue-journal-layer first, then copy the shared files to the other two layers. Start with `git status --porcelain` in each of the three layers and stop if any has unrelated uncommitted work.
|
||||
|
||||
1. RED first (journal). Write `testing/purge-token-check.mjs` in the style of `testing/purge-check.mjs` (plain .mjs with JSDoc, args `--output`, `--fixtures`, `--path`, `startFixtureApi` and `startNuxtServer`, last line `purge-token-check: ok (<n> checks)`, each failure `purge-token-check: FAIL ...` and exit 1). It covers the three scenarios from the behavior block. Tokens are fresh `randomBytes(32).toString('hex')` values. Failure messages name the key or say "token value", never the value. Fetch `/` and `--path`, plus `<path>/_payload.json` when it answers 200. Make one additive change to `testing/nuxt-server.mjs`: the returned object also gets `output()`, which returns the child's captured stdout and stderr so far (`url` and `close` stay as they are; apps import this file). B's deprecation-warning assertion uses it. Run `pnpm build:playground` at the current HEAD, then run the script. It must FAIL: B leaks cachePurgeToken, and A's purge answers 404 because cachePurge is not read yet. Keep the FAIL lines for the SUMMARY as RED evidence.
|
||||
|
||||
2. Shared lib `server/lib/siteCachePurge.ts`. This is additive: existing exports and their behaviour stay. Add:
|
||||
- `SITE_CONFIG_PURGE_KEYS`: the readonly list ['cachePurgeToken', 'cacheAllowProxied'], the keys nuxt-site-config derives from the deprecated variables.
|
||||
- `SITE_PURGE_WARNED = Symbol.for('summercms.siteCachePurge.deprecationWarned')`.
|
||||
- `resolveSitePurgeConfig(config: unknown)`, returning `{ token: unknown; allowProxied: boolean; deprecated: ('token' | 'allowProxied')[] }` with the QD-02 rules. It reads `cachePurge.token` and `cachePurge.allowProxied`, and falls back to `siteCache.purgeToken` and `siteCache.allowProxied`.
|
||||
- `redactSiteConfigStack(stack: unknown): number` (QD-03).
|
||||
- `sitePurgeDeprecationMessage(deprecated)`, returning '' for an empty list. Otherwise it returns one line starting `[site-cache]` that names each deprecated variable in use with its replacement (NUXT_SITE_CACHE_PURGE_TOKEN -> NUXT_CACHE_PURGE_TOKEN, NUXT_SITE_CACHE_ALLOW_PROXIED -> NUXT_CACHE_PURGE_ALLOW_PROXIED). It says that earlier layer versions published NUXT_SITE_* values in the page payload through nuxt-site-config, so a token set under the old name must be rotated.
|
||||
Update the header comment: the file is pure TypeScript, shipped byte for byte by three layers.
|
||||
|
||||
3. Shared route `server/api/__site__/cache/purge.post.ts`: take the token and allowProxied from `resolveSitePurgeConfig(useRuntimeConfig(event))` and pass them to `authorizeSitePurge`. The answers (404/401/200), the log line and the no-store header stay the same. The header comment names `runtimeConfig.cachePurge.token` (NUXT_CACHE_PURGE_TOKEN) and `cachePurge.allowProxied` (NUXT_CACHE_PURGE_ALLOW_PROXIED), with the siteCache.* names as deprecated for one release.
|
||||
|
||||
4. Shared plugin `server/plugins/site-cache-purge.ts`: `defineNitroPlugin((nitroApp) => { ... })`. It imports the lib by relative path with the .ts extension, as the route does. In the plugin function body (not at module top level, so tests can vary the config), it reads `useRuntimeConfig()`, gets `deprecated` from resolveSitePurgeConfig, and calls console.warn with sitePurgeDeprecationMessage once per process, guarded by `globalThis[SITE_PURGE_WARNED]`. It registers `site-config:init` through a loose cast of `nitroApp.hooks` (the hook name is untyped in layers without nuxt-site-config): the handler calls redactSiteConfigStack on `ctx?.siteConfig?.stack`. The file names no layer. Its comment explains the NUXT_SITE_* mechanism and why the stripping stays even for the new names.
|
||||
|
||||
5. Each layer's `nuxt.config.ts` (these files differ between layers, so edit each one): add `runtimeConfig.cachePurge: { token: '', allowProxied: false }`, with comments naming NUXT_CACHE_PURGE_TOKEN (server only, at least 32 characters, the backend's site_cache.purge_token) and NUXT_CACHE_PURGE_ALLOW_PROXIED. The comment says why the name is not under NUXT_SITE_: nuxt-site-config publishes those in every page's payload. Keep `siteCache: { purgeToken: '', allowProxied: false }` so Nuxt still maps the old variables, with a "Deprecated (one release)" comment pointing to cachePurge.*.
|
||||
|
||||
6. Tests: extend `tests/sitePurge.test.ts` with the behavior specs. Test names start with "sitePurge:". The `callRoute` stub gains a cachePurge option. The plugin specs stub `defineNitroPlugin` (identity), `useRuntimeConfig` and console.warn on globalThis, the way callRoute stubs Nitro, and they delete `globalThis[SITE_PURGE_WARNED]` between cases. Add `SHARED_PLUGIN_SHA256` and a third assertion to the pin test. Update the pin comment to three files. Write the files, compute the hashes with sha256sum, then write the constants.
|
||||
|
||||
7. Copy the lib, the route, the plugin and `tests/sitePurge.test.ts` from vue-journal-layer into vue-menu-layer and vue-faq-layer byte for byte (`cp`, then `cmp` each pair).
|
||||
|
||||
8. READMEs, in the same commit as the code per the docs rule:
|
||||
- Configuration tables: new rows for `runtimeConfig.cachePurge.token` / `NUXT_CACHE_PURGE_TOKEN` and `runtimeConfig.cachePurge.allowProxied` / `NUXT_CACHE_PURGE_ALLOW_PROXIED`. The siteCache.* rows are marked "Deprecated (one release)".
|
||||
- "Site cache purge": every primary mention uses the new names. Add the upgrade paragraph, "Upgrading from 0.5" in the journal README and "from 0.3" in menu and faq. It says: rename the two variables; the old names work through this minor and go in the next; why (nuxt-site-config publishes NUXT_SITE_*); the layer now strips the two keys from the site config even under the old names; rotate a token that was ever set under the old name, since earlier versions published it.
|
||||
- The byte-for-byte sentence lists three shared files.
|
||||
- Journal "Testing" gains `testing/purge-token-check.mjs` in the list and in the command block.
|
||||
Keep the old `/api/__journal__/cache/purge` alias and its `NUXT_JOURNAL_CACHE_PURGE_TOKEN` exactly as documented (not a NUXT_SITE_ name, untouched here).
|
||||
|
||||
9. package.json `version`: vue-journal-layer 0.6.0, vue-menu-layer 0.4.0, vue-faq-layer 0.4.0 (QD-05).
|
||||
|
||||
10. GREEN: in each layer run `pnpm test`. In the journal, run `pnpm build:playground`, then `node testing/purge-token-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --path /blog/hello-acme`, plus the existing smoke-ssr and purge-check commands from its README (both must still pass). In menu and faq, run `pnpm build:playground && pnpm smoke:ssr`, which proves the plugin builds and stays inert without nuxt-site-config.
|
||||
|
||||
11. Commit one commit per layer, with code, tests, README and version together, staged by explicit path. Message: `fix: keep the site cache purge token out of the page payload (NUXT_CACHE_PURGE_TOKEN)`, a body summarising QD-01..QD-04, and no trailer. No tag, no push. Confirm with `git ls-remote --tags origin` that v0.6.0 (journal) and v0.4.0 (menu, faq) are still free, and note the three commit hashes for the checkpoint.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms && for r in vue-journal-layer vue-menu-layer vue-faq-layer; do (cd $r && pnpm test >/dev/null 2>&1 && echo "$r: tests ok") || exit 1; done && for f in server/lib/siteCachePurge.ts server/api/__site__/cache/purge.post.ts server/plugins/site-cache-purge.ts tests/sitePurge.test.ts; do sha256sum vue-journal-layer/$f vue-menu-layer/$f vue-faq-layer/$f | awk '{print $1}' | sort -u | wc -l; done && cd vue-journal-layer && pnpm build:playground >/dev/null && node testing/purge-token-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --path /blog/hello-acme | tail -n1 && node testing/purge-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --kind post --slug hello-acme --path /blog/hello-acme | tail -n1 && cd ../vue-menu-layer && pnpm build:playground >/dev/null && pnpm smoke:ssr | tail -n1 && cd ../vue-faq-layer && pnpm build:playground >/dev/null && pnpm smoke:ssr | tail -n1</automated>
|
||||
</verify>
|
||||
<done>Every layer prints `<layer>: tests ok`. Each of the four shared paths prints `1` (identical in the three layers). The output includes `purge-token-check: ok (...)`, `purge-check: ok (...)` and the two smoke ok lines. `grep -c 'NUXT_CACHE_PURGE_TOKEN'` is at least 1 in each layer's nuxt.config.ts and README.md. package.json versions are 0.6.0 / 0.4.0 / 0.4.0. Each layer has exactly one new local commit whose message (`git log -1 --format=%B`) has no co-author or other trailer line, `git status --porcelain` is empty, and nothing is tagged or pushed. The RED FAIL lines from step 1 are kept for the SUMMARY.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 2: Go-ahead to tag and push the three public layers</name>
|
||||
<files>none (git refs only: refs/tags/v0.6.0 in vue-journal-layer, refs/tags/v0.4.0 in vue-menu-layer and vue-faq-layer, and their main branches on git.golem15.com)</files>
|
||||
<action>Stop and present the decision. Nothing is tagged or pushed before the reply. Show the user, for each layer: `git log --oneline origin/main..main` (the one Task 1 commit with its hash), the diffstat, the test and E2E results, and the RED evidence. Then list the exact operations Task 3 will run after a "push": (1) annotated tags vue-journal-layer `v0.6.0`, vue-menu-layer `v0.4.0` and vue-faq-layer `v0.4.0` on those commits, each with the message "v0.6.0 (or v0.4.0): the site cache purge token moves to runtimeConfig cachePurge.token (NUXT_CACHE_PURGE_TOKEN) and cachePurge.allowProxied (NUXT_CACHE_PURGE_ALLOW_PROXIED); the NUXT_SITE_CACHE_* names keep working for one release, and the layer strips them from nuxt-site-config's site config, which published them in the page payload"; (2) `git push origin main` and `git push origin <tag>` in each of the three public repositories, never forced; (3) after the demo commits, the normal pushes of the private vue-demo-app (master) and sm-demo-app (master). It also says that tags are never moved once pushed, and that summercms.io and grzybyfunkcjonalne are not touched.</action>
|
||||
<decision>Tag and push vue-journal-layer v0.6.0, vue-menu-layer v0.4.0 and vue-faq-layer v0.4.0 (public on git.golem15.com), then push the private vue-demo-app and sm-demo-app after Task 3's commits?</decision>
|
||||
<context>The layers are public and consumed by tag. Pushed tags are permanent (QD-05). Consumers keep working on the old env names for one release (QD-02), but any site on an older layer that sets NUXT_SITE_CACHE_PURGE_TOKEN keeps publishing it until it moves to these tags. The demo bump and the rome rotation (Tasks 3-5) need the tags on the remote.</context>
|
||||
<options>
|
||||
<option id="push">
|
||||
<name>Tag and push as listed</name>
|
||||
<pros>Unblocks the demo bump and the rotation; consumers can move to the fixed layers.</pros>
|
||||
<cons>Outward-facing and permanent; the deprecation becomes public.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Keep everything local</name>
|
||||
<pros>Nothing leaves the machine; the commits can still change.</pros>
|
||||
<cons>Tasks 3-5 do not run; demo.summercms.io keeps publishing its token until a later go-ahead.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply "push" (optionally naming different tag numbers) or "hold".</resume-signal>
|
||||
<verify>The reply is recorded verbatim in the SUMMARY. Before "push", `git -C S/vue-journal-layer ls-remote --tags origin v0.6.0` and the same for v0.4.0 in menu and faq print nothing.</verify>
|
||||
<done>The user answered. On "hold" the plan ends here: write the SUMMARY with the local commit hashes and the open follow-ups, and run no later task.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Demo - the layer tags pushed, vue-demo-app on them, the env renamed in sm-demo-app, and three checks that refuse a purge token in a page</name>
|
||||
<precondition>Task 2 was answered "push" (with the tag numbers it named).</precondition>
|
||||
<reversibility rating="one-way">Pushed public tags are never moved or reused; the decision checkpoint (Task 2) gates them.</reversibility>
|
||||
<files>../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/pnpm-lock.yaml, ../sm-demo-app/vue-demo-app/README.md, ../sm-demo-app/vue-demo-app (submodule pointer), ../sm-demo-app/deploy/site.env.example, ../sm-demo-app/deploy/env.example, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/README.md, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/scripts/check-deploy.sh, ../sm-demo-app/scripts/smoke.sh, docs/services/caching.md, modules/cooler/README.md, .planning/todos/pending/2026-10-20-summercmsio-changelog-purge-token-public.md, .planning/todos/pending/2026-10-20-rename-site-cache-purge-env-in-consumers.md</files>
|
||||
<action>
|
||||
A. Tags and pushes (QD-05). In each layer, check that `git status --porcelain` is empty and HEAD is the Task 1 commit. Create the annotated tag with the Task 2 message (`git tag -a <tag> -m ...`), then `git push origin main` and `git push origin <tag>`. Never force. Verify that `git ls-remote origin 'refs/tags/<tag>^{}'` equals `git rev-parse HEAD`.
|
||||
|
||||
B. HTTP probe, counts only (goal 4: why summercms.io and grzybyfunkcjonalne looked clean). For https://demo.summercms.io/, https://summercms.io/, https://summercms.io/changelog, https://uat.grzybyfunkcjonalne.pl/ and https://grzybyfunkcjonalne.pl/, record the HTTP status and `grep -o cachePurgeToken | wc -l`. Never print a matching line. Expected from planning: demo 4, summercms.io/ 0 (Go landing page), summercms.io/changelog 4 (the changelog Nuxt app leaks too), uat 401 Basic auth, grzybyfunkcjonalne.pl the PHP site.
|
||||
|
||||
C. vue-demo-app: use the submodule at S/sm-demo-app/vue-demo-app, never S/vue-demo-app. Check it is clean and on master. Set the package.json pins to `#v0.6.0` (journal), `#v0.4.0` (menu) and `#v0.4.0` (faq), then run `pnpm install`. Check that the pnpm-lock.yaml diff touches only those three layer entries and that each records its tag's commit. Update the version column of the README table. Run `pnpm test` (vitest). Commit with `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)`, staging package.json, pnpm-lock.yaml and README.md by path.
|
||||
|
||||
D. sm-demo-app edits (QD-06). Start with `git status --porcelain` and stop on unrelated work.
|
||||
- `deploy/site.env.example`: rename the key line to `NUXT_CACHE_PURGE_TOKEN=`. Its comment says the value equals SUMMER_SITE_CACHE__PURGE_TOKEN and explains why the name is not under NUXT_SITE_ (nuxt-site-config publishes every such variable in the page payload; rotated on 2026-10-20).
|
||||
- `deploy/env.example`: the SUMMER_SITE_CACHE__PURGE_TOKEN comment names NUXT_CACHE_PURGE_TOKEN in site.env.
|
||||
- `DEPLOY.md`: the one-time setup's printf writes NUXT_CACHE_PURGE_TOKEN. Update the Secrets table row. Add a "Rotating the site-cache purge token" subsection under Secrets: hex token generated on rome into a variable; delete both the old and the new key line from site.env and append the new one; replace SUMMER_SITE_CACHE__PURGE_TOKEN in .env; check the counts, a sha256 equality verdict and the file modes; one restart of both programs. Add a dated note that the key was NUXT_SITE_CACHE_PURGE_TOKEN until vue-journal-layer 0.6.0 and was published in every page's payload, so it was rotated. Extend the Verify paragraph with the new check-live assertion. Add one Rollback sentence: a site build on vue-journal-layer older than 0.6.0 reads no purge token (route 404, Go logs purge failures) until the next release; the demo does not route-cache pages, so nothing goes stale.
|
||||
- `scripts/check-deploy.sh`: the header comment gains a bullet. The Python block fails when a non-comment line of site.env.example has a key starting with `NUXT_SITE_` (message: nuxt-site-config publishes NUXT_SITE_* in the page payload; NUXT_PUBLIC_SITE_URL is the intended public key). It requires a `NUXT_CACHE_PURGE_TOKEN=` line with an empty value, and fails when a `printf` line in DEPLOY.md writes a `NUXT_SITE_` key.
|
||||
- `scripts/smoke.sh`: generate two throwaway hex tokens. Pass one as `NUXT_CACHE_PURGE_TOKEN` and the other as the deprecated `NUXT_SITE_CACHE_PURGE_TOKEN` in the `env -i` Nuxt start; the deprecated one proves the site-config stripping in the real four-layer app. In the server-rendered page loop and for the client-only `/p/roadmap-$HASH` page (window.__NUXT_SITE_CONFIG__), fail when the body contains either value (`grep -qF`, with a message that does not print the value) or matches `grep -qiE 'cache_?purge|purge_?token|cache_?allow_?proxied'`. After the pages, a loopback POST to `$SITE/api/__site__/cache/purge` with the new bearer must answer 200 with `"purged"`, and with the deprecated bearer 401 (the new one wins). The site log must contain no token value and exactly one `[site-cache]` deprecation line. Update the header comment.
|
||||
- `scripts/check-live.sh`: add a `no_purge_key LABEL` helper next to `no_admin_link`. It fails when the last body matches the same case-insensitive pattern, and its message prints only the matched key name (`grep -oiE` of the pattern, which never spans a value). Call it wherever `no_admin_link` is called and after the public deck page GET (an ssr:false page carrying window.__NUXT_SITE_CONFIG__). Update the header comment.
|
||||
- `README.md`: the smoke, check-deploy and check-live descriptions in Release mention the new assertions.
|
||||
Commit the docs and scripts as `fix: the site's purge token is NUXT_CACHE_PURGE_TOKEN; checks refuse a purge token in any page`. Then commit the pointer separately: `git add vue-demo-app` (the gitlink only, nothing inside the submodule), message `chore: bump vue-demo-app (layers v0.6.0, v0.4.0, v0.4.0)`.
|
||||
|
||||
E. Gate. In sm-demo-app run `scripts/check-deploy.sh`, `scripts/build.sh release` and `scripts/smoke.sh`. smoke.sh needs Docker; if the socket is denied, run it through `sudo -u golem` per CLAUDE.md. If build.sh refuses a stale ROADMAP.md snapshot, run `scripts/sync-roadmap.sh` and commit `chore: sync the ROADMAP.md snapshot (summercms <sha>)` on its own, then rebuild. Then run `scripts/check-live.sh https://demo.summercms.io` against the still-leaking live site: it must fail with the new purge-key lines (record their count and the key name only) and with no other failure. This is the check's RED evidence. The live fix happens in Task 5.
|
||||
|
||||
F. Pushes (normal flow after the Task 2 go-ahead): vue-demo-app master first, then sm-demo-app master. Never force. Check `git ls-remote origin master` against HEAD in both.
|
||||
|
||||
G. Framework docs in summercms.go (QD-08), committed, never pushed. First check `git status --short docs/services/caching.md modules/cooler/README.md`. Update docs/services/caching.md (lines ~170-178) and the modules/cooler/README.md rows for site_cache.purge_url and site_cache.purge_token: the site's variables are NUXT_CACHE_PURGE_TOKEN and NUXT_CACHE_PURGE_ALLOW_PROXIED, with one sentence on why not NUXT_SITE_* and that the layers accept the old names for one release. Name no consuming application. Run `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check`. Commit `docs(cooler): the site's purge token variable is NUXT_CACHE_PURGE_TOKEN`.
|
||||
|
||||
H. Todos (QD-08). Write two files in .planning/todos/pending/ following the frontmatter of site-cache-purge-for-menu-and-faq.md (created, title, area, severity, `g15office: TBD` for the orchestrator to fill, files), each with Problem and Solution sections:
|
||||
- `2026-10-20-summercmsio-changelog-purge-token-public.md`, severity major (security). Evidence: summercms.io/changelog publishes cachePurgeToken (the probe count from B). Its nginx and the route's proxied refusal make it unusable from outside today. Steps, each needing the user's go-ahead because this is production: move the changelog app to vue-journal-layer v0.6.0; rename the variable in /srv/summercms-io/changelog.env and rotate the value in both files on rome (the demo's DEPLOY.md subsection is the recipe); update deploy/changelog.env.example, deploy/env.example, DEPLOY.md, changelog/nuxt.config.ts, the vue-summercmsio-app README and the supervisor comment.
|
||||
- `2026-10-20-rename-site-cache-purge-env-in-consumers.md`, severity minor. grzybyfunkcjonalne: deploy/nuxt.env.example, DEPLOY.md, README, the layer pins (journal v0.4.0, menu and faq v0.2.0); check the UAT payload with the UAT credentials (anonymous requests get 401) and the server env by key-name count; rotate if the old name was set. The plugin READMEs that name the old variable (sm-journal-plugin "Receiver", sm-menu-plugin, sm-faq-plugin). Drop the deprecated names in the next layer minors (journal 0.7.0, menu and faq 0.5.0) only after every consumer moved, and then revisit smoke.sh's deprecated-token assertions.
|
||||
Commit both alone: `docs(todos): purge token rename follow-ups for summercms.io and grzybyfunkcjonalne`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms && git -C vue-journal-layer ls-remote origin 'refs/tags/v0.6.0^{}' && git -C vue-menu-layer ls-remote origin 'refs/tags/v0.4.0^{}' && git -C vue-faq-layer ls-remote origin 'refs/tags/v0.4.0^{}' && cd sm-demo-app && grep -c 'vue-journal-layer.git#v0.6.0\|vue-menu-layer.git#v0.4.0\|vue-faq-layer.git#v0.4.0' vue-demo-app/package.json && grep -v '^#' deploy/site.env.example | grep -c '^NUXT_SITE_' ; grep -c '^NUXT_CACHE_PURGE_TOKEN=$' deploy/site.env.example && bash -n scripts/check-live.sh scripts/check-deploy.sh scripts/smoke.sh && scripts/check-deploy.sh | tail -n1 && (cd vue-demo-app && pnpm test >/dev/null 2>&1 && echo "vitest ok") && ST=$(git status --porcelain) && test -z "$ST" && echo clean && R=$(git ls-remote origin refs/heads/master) && test "$(git rev-parse HEAD)" = "${R%%[[:space:]]*}" && echo pushed && cd ../summercms.go && go test ./cmd/summer -run TestDocsTree</automated>
|
||||
</verify>
|
||||
<done>The three tags resolve on the remote to the Task 1 commits. The package.json grep prints 3, the NUXT_SITE_ count prints 0, the NUXT_CACHE_PURGE_TOKEN line count prints 1, and `check-deploy: ok`. `vitest ok`, `clean` and `pushed` print (sm-demo-app clean and pushed), with vue-demo-app pushed before it. `scripts/build.sh release` ended `build: ok release ...` and `scripts/smoke.sh` ended `smoke: ok`, recorded in the SUMMARY. check-live against the live site failed only with purge-key lines (count and key name recorded). The HTTP probe counts are recorded. The framework docs commit is local only and TestDocsTree passes. The two todos are committed. No commit has a trailer line.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking-human">
|
||||
<name>Task 4: Stop before the first SSH command to rome</name>
|
||||
<files>none (remote state on rome: /srv/summercms-demo)</files>
|
||||
<action>Stop here. No ssh or rsync command runs before the user's reply, because in auto mode SSH is rejected. Present what is done (layer tags, the demo release build in sm-demo-app/bin/demo and vue-demo-app/.output, smoke ok, pushes) and the exact remote operations Task 5 will run: the three rsync uploads from DEPLOY.md "Upload"; one `ssh -p 2222 debian@10.8.3.118 'bash -s'` script that migrates and seeds with bin/demo.new, rotates the token (QD-07: `openssl rand -hex 32` into a variable, the old and new key lines removed from site.env, NUXT_CACHE_PURGE_TOKEN appended, SUMMER_SITE_CACHE__PURGE_TOKEN replaced in .env, counts, a sha256 equality verdict and stat as the only output), swaps the binary and the site, and restarts summercms-demo and summercms-demo-site; then a loopback purge check and a value check on rome, and check-live from this machine. Explain that no secret is printed, and that the old token becomes invalid with the restart.</action>
|
||||
<instructions>Claude has built, smoke-tested and pushed everything that does not need rome. The one human step is to let this session use SSH to rome (switch it out of auto mode, or approve the SSH commands), or to run Task 5's remote steps yourself from DEPLOY.md's new "Rotating the site-cache purge token" subsection together with the Release section.</instructions>
|
||||
<verification>After "ssh-ok", Task 5 runs everything and verifies. After "done-by-me", Task 5 runs only its local HTTP verification (step 4). After "skip", nothing runs on rome and the SUMMARY records that demo.summercms.io still publishes its token.</verification>
|
||||
<resume-signal>Reply "ssh-ok", "done-by-me" or "skip".</resume-signal>
|
||||
<verify>No ssh or rsync to rome appears in this session's commands before the reply.</verify>
|
||||
<done>The user's reply is recorded verbatim in the SUMMARY.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Rotation on rome - upload and release the new build with a new purge token under NUXT_CACHE_PURGE_TOKEN</name>
|
||||
<precondition>Task 4 was answered "ssh-ok" (for "done-by-me" run step 4 only; for "skip" run nothing).</precondition>
|
||||
<reversibility rating="costly">The rotation and the restart change the live demo; a further rotation or DEPLOY.md "Rollback" undoes them, and the old token is never restored.</reversibility>
|
||||
<files>none in a repository; remote state: /srv/summercms-demo/site.env, /srv/summercms-demo/.env, /srv/summercms-demo/bin/demo, /srv/summercms-demo/site/.output (and the .prev copies the release keeps)</files>
|
||||
<action>
|
||||
Use ROME=debian@10.8.3.118 and RSH='ssh -p 2222' (DEPLOY.md). Every remote command names the env files through the globs `.en[v]` and `site.en[v]`. Every remote script starts with `set -euo pipefail` and `shopt -s failglob`, so a missing file stops the script instead of creating a file named after the pattern. No command prints a token.
|
||||
|
||||
1. Pre-flight (local): `git -C S/sm-demo-app status --porcelain` is empty, and bin/demo and vue-demo-app/.output come from Task 3's `build.sh release` at the pushed HEAD (rebuild with `scripts/build.sh release` if HEAD moved). `grep -rl cacheAllowProxied vue-demo-app/.output/server | head -n1` finds the stripping code in the bundle.
|
||||
|
||||
2. Upload: the three rsync commands of DEPLOY.md "Upload" verbatim.
|
||||
|
||||
3. Release with rotation: one `$RSH $ROME 'bash -s'` call fed a quoted heredoc, run as debian with sudo inside, in /srv/summercms-demo:
|
||||
(a) `migrate` and `demo:seed` with bin/demo.new, as DEPLOY.md "Release" does.
|
||||
(b) Rotation: `PT=$(openssl rand -hex 32)`. Run `sudo sed -i -e '/^NUXT_SITE_CACHE_PURGE_TOKEN=/d' -e '/^NUXT_CACHE_PURGE_TOKEN=/d' site.en[v]`, then `printf 'NUXT_CACHE_PURGE_TOKEN=%s\n' "$PT" | sudo tee -a site.en[v] >/dev/null`. Run `sudo sed -i '/^SUMMER_SITE_CACHE__PURGE_TOKEN=/d' .en[v]`, then `printf 'SUMMER_SITE_CACHE__PURGE_TOKEN=%s\n' "$PT" | sudo tee -a .en[v] >/dev/null`. Then `unset PT`.
|
||||
(c) Checks that print counts and verdicts only. `sudo grep -c '^NUXT_CACHE_PURGE_TOKEN=' site.en[v]` must be 1. `sudo grep -c '^NUXT_SITE_' site.en[v] || true` must be 0. `sudo grep -c '^SUMMER_SITE_CACHE__PURGE_TOKEN=' .en[v]` must be 1. Compare the sha256sum of `sudo sed -n 's/^NUXT_CACHE_PURGE_TOKEN=//p' site.en[v]` with the sha256sum of `sudo sed -n 's/^SUMMER_SITE_CACHE__PURGE_TOKEN=//p' .en[v]`: print `purge token: match` or exit 1. `stat -c '%a %U:%G %n' .en[v] site.en[v]` must show 640 root:summercms-demo for both. If any check fails, the script exits before the swap: the running programs keep their old environment in memory, so the site stays up. Report the failure and stop.
|
||||
(d) Swap and restart exactly as in DEPLOY.md "Release": bin/demo.prev, the site/.output swap, `sudo supervisorctl restart summercms-demo summercms-demo-site`, `websockets:health`.
|
||||
(e) On rome, wait until 127.0.0.1:3098 answers. Read the token into a variable from site.en[v] with sed. POST `{"kind":"faq","slug":"rotation-check"}` to `http://127.0.0.1:3098/api/__site__/cache/purge` with the Authorization header from a process substitution (`-H @<(printf 'Authorization: Bearer %s\n' "$T")`, keeping it off argv): it must answer 200. Without the header it must answer 401. For `/`, `/blog`, `/faq` and `/contact` fetched on loopback, `grep -cF "$T"` must be 0 and `grep -c cachePurgeToken` must be 0. Then `unset T`. `sudo tail -n 200 /var/log/supervisor/summercms-demo-site.log | grep -c 'deprecated' || true` must be 0 (no old name in use). `sudo tail -n 100 /var/log/supervisor/summercms-demo.log | grep -c 'site cache purge enabled'` must be at least 1.
|
||||
If the restart or (e) fails, follow DEPLOY.md "Rollback" for the app. The rotated values stay; the old site build then has no purge token, which is acceptable per the Rollback note.
|
||||
|
||||
4. Local HTTP verification: `scripts/check-live.sh https://demo.summercms.io` prints `check-live: ok`. The `grep -o cachePurgeToken | wc -l` count is 0 for /, /blog, /faq and the deck page /roadmap redirects to (follow the Location).
|
||||
|
||||
5. In the SUMMARY record only counts, the `purge token: match` verdict, the stat lines, the HTTP codes and the check-live last line, plus the note for the 19.4.1 UAT that verification item 1 is resolved by option (a). The orchestrator records it; never edit .planning/phases/19.4.1-*.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/sm-demo-app && scripts/check-live.sh https://demo.summercms.io 2>&1 | tail -n1 && for p in / /blog /faq /contact; do curl -s --max-time 20 "https://demo.summercms.io$p" | grep -o cachePurgeToken | wc -l; done</automated>
|
||||
</verify>
|
||||
<done>`check-live: ok` and four zeros. The remote script printed 1, 0 and 1 for the key counts, `purge token: match`, two `640 root:summercms-demo` stat lines, purge 200 with the token and 401 without, 0 for each page's value and key count on loopback, 0 deprecation lines, and at least 1 Go `site cache purge enabled` line. No token value appears in any command output or in the SUMMARY.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Nuxt server runtime config -> public page payload | Server-only env values must not cross into useState('site-config') / window.__NUXT_SITE_CONFIG__ |
|
||||
| internet -> nginx -> Nuxt POST /api/__site__/cache/purge | Untrusted callers; the route must stay loopback-only unless a site opts in |
|
||||
| build machine -> rome over SSH | Secret generation and file edits happen on rome; values must not travel back into logs, chat or files here |
|
||||
| framework layers (public tags) -> consumers | A tag is permanent; consumers on old env names must keep working |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-sdt-01 | I | layer runtime config read by nuxt-site-config (cachePurgeToken, cacheAllowProxied in the payload) | high | mitigate | QD-01 rename outside NUXT_SITE_; QD-03 site-config:init plugin strips both keys even under the old names; purge-token-check.mjs (journal), smoke.sh (demo build, values and keys) and check-live.sh (live, keys) assert nothing leaks |
|
||||
| T-sdt-02 | I | rotation on rome (shell history, command output, chat, SUMMARY) | high | mitigate | token generated on rome into a variable (openssl rand -hex 32) and written with printf into sudo tee; checks print counts, a sha256 equality verdict and stat only; curl reads the bearer from a process substitution, not argv; env files named through globs with failglob; the SUMMARY records no value |
|
||||
| T-sdt-03 | E | POST /api/__site__/cache/purge with the published demo token | medium | mitigate | rotation invalidates the published value; nginx `limit_except GET HEAD` and the route's proxied-request refusal stay; the demo sets no allowProxied |
|
||||
| T-sdt-04 | D | Go (.env) and Nuxt (site.env) holding different tokens after rotation | low | mitigate | one value written to both in one remote script under set -e; sha256 equality checked before the restart; loopback purge 200 after it |
|
||||
| T-sdt-05 | D | consumers on the old env names (grzybyfunkcjonalne production, summercms.io) | high | mitigate | QD-02 fallback keeps the old names working for one release, covered by route and resolver specs and smoke's deprecated-token run; no consumer deploy; follow-up todos |
|
||||
| T-sdt-06 | T | public layer tags | medium | mitigate | blocking-human decision checkpoint (Task 2) listing the exact tags and commits; annotated tags on tested commits; never moved, never force-pushed |
|
||||
| T-sdt-07 | I | summercms.io/changelog still publishing its production token | high | transfer | out of scope by the brief (no consumer change); recorded as a major todo with evidence for the user's decision |
|
||||
| T-sdt-SC | T | npm/pnpm installs | low | accept | no new dependency; pnpm install in vue-demo-app only re-resolves the three golem15 layers from their new git tags, and the lockfile diff is checked to touch only those entries |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Layers: `pnpm test` green in all three; the four shared paths byte-identical; journal `purge-token-check: ok`, `purge-check: ok` and smoke-ssr ok on the built playground; menu and faq playgrounds build and pass smoke:ssr.
|
||||
- Tags v0.6.0 / v0.4.0 / v0.4.0 on the remote only after "push" at Task 2.
|
||||
- Demo: `check-deploy: ok`, `build: ok release`, `smoke: ok`, vitest green, sm-demo-app and vue-demo-app pushed; check-live failed only on purge-key lines before the rotation.
|
||||
- Rotation: remote counts 1/0/1, `purge token: match`, modes 640 root:summercms-demo, loopback purge 200/401, no value or key on pages; `check-live: ok` after.
|
||||
- summercms.go: TestDocsTree and `docs:build --check` pass; framework docs and todos committed, nothing pushed; STATE.md, state.json and .planning/phases/19.4.1-* untouched.
|
||||
- No secret value in any output, commit or the SUMMARY; no trailer line in any commit.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- No public page of demo.summercms.io contains the purge token or a purge key (check-live and curl counts 0), with the token rotated on rome.
|
||||
- Any site on vue-journal-layer 0.6.0 / vue-menu-layer 0.4.0 / vue-faq-layer 0.4.0 stops publishing the token whatever env name it uses, and existing consumers keep purging on the old names for one release.
|
||||
- The demo's committed files and checks make a regression fail (check-deploy, smoke, check-live).
|
||||
- summercms.io and grzybyfunkcjonalne are untouched, with follow-up todos recorded (the summercms.io changelog exposure flagged as major).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/261020-sdt-SUMMARY.md` when done. Include: commits per repository with hashes (three layers, vue-demo-app, sm-demo-app, summercms.go docs, todos), the pushed tags and the remote hashes they resolve to, the shared-file sha256 pins, the RED evidence (purge-token-check FAIL lines before the fix, check-live purge-key failures before the rotation), the replies to Tasks 2 and 4 verbatim, the HTTP probe counts, the rotation evidence (counts, verdict, stat lines, HTTP codes, check-live last line), and the follow-ups: the two todos (their g15office IDs are written back by the orchestrator), and 19.4.1 verification item 1 resolved by option (a) for the orchestrator to record. Never a secret value.
|
||||
</output>
|
||||
Reference in New Issue
Block a user