From b6d6b6132231c49fbaf126765cf220dc87d6fcee Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:03:23 -0400 Subject: [PATCH 1/3] docs(roadmap): record the #72 and #73 promotion to production --- ROADMAP.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e7f1162..95b7f1e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,24 +3,23 @@ Loose, personal roadmap for the CodesWhat homepage. Not a ticket tracker (that's work). Just the ideas worth not forgetting. -## Where things stand (2026-10-25) +## Where things stand (2026-10-25, evening) The Catalogue redesign, the `/support` page and the analytics events shipped on -2026-10-22 (#69, #70, #71). Production is `main@68520ee`. Two things are in -flight: - -- [ ] Dependency security patch (Next 16.4.3, dompurify, sharp): PR #72 is green - on CI and preview and waiting on a delivered review before it lands in `dev`, - then a promotion to `main`. -- [ ] CI hardening (support tests in `verify`, no duplicate promotion runs, - fork-safe skip, no persisted checkout credential, actions pinned to - commits): finished and tested locally, queued behind #72. -- [ ] Remove the stray `visual-schedule-manager-portfolio/` app and root - `start.sh`; nothing references either. +2026-10-22 (#69, #70, #71). On 2026-10-25 the dependency security patch (#72: +Next 16.4.3, dompurify 3.4.17, sharp 0.35.5, which cleared every open Dependabot +alert) and the CI hardening batch (#73: support tests in `verify`, no duplicate +promotion runs, fork-safe skip, no persisted checkout credential, actions pinned +to commits, stray `visual-schedule-manager-portfolio/` and `start.sh` removed) +were promoted together in #74. Production is `main@722af11`, checked in a +browser after the deploy. + +Still open: + - [ ] Clean up `test/posthog.test.ts`, which imports a posthog-js internal path, then bump `posthog-js` and add `test:posthog:browser` to CI. -- [ ] Close #58 (`.claude/` is already ignored on `main`). #57 is partly done; - the remaining items are tracked privately. +- [ ] #57 is partly done (pins, credentials, CSP and input tests); the remaining + items are tracked privately. ## Progressive-enhancement fallback for the 3D layer From 377988f82731b1b529d4cabffc1c6c0c5c6fb0d0 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:34:37 -0400 Subject: [PATCH 2/3] test(analytics): repair the PostHog contract tests - test(analytics): resolve posthog-core by package specifier and fail with a clear message if an upgrade drops PostHog#_runBeforeSend - test(analytics): replace the stale body check in the production browser contract; Next's flight data carries the requester's own query, so assert the HTML isn't shared-cacheable and that nothing outside the flight-data scripts reflects it - docs(deploy): run the browser contract after each promotion --- frontend/README-VERCEL.md | 6 ++++++ frontend/test/posthog-browser.test.mjs | 12 +++++++++++- frontend/test/posthog.test.ts | 10 +++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/README-VERCEL.md b/frontend/README-VERCEL.md index 5472b18..4c7a821 100644 --- a/frontend/README-VERCEL.md +++ b/frontend/README-VERCEL.md @@ -13,6 +13,12 @@ and persists after reload, the specimen plates render, and `/support` shows its buy links. Build success and analytics events alone do not establish that client initialization worked. +Then run the PostHog response contract against the canonical domain: + +```sh +POSTHOG_BROWSER_BASE_URL=https://codeswhat.com npm run test:posthog:browser +``` + `npm run og` regenerates the share cards in `public/og*` with playwright-core and the locally installed Google Chrome; it isn't part of the build. diff --git a/frontend/test/posthog-browser.test.mjs b/frontend/test/posthog-browser.test.mjs index 3c02e7b..471ece3 100644 --- a/frontend/test/posthog-browser.test.mjs +++ b/frontend/test/posthog-browser.test.mjs @@ -14,6 +14,16 @@ test("production browser response keeps PostHog on the exact proxy", { assert.match(csp, /script-src[^;]*https:\/\/e\.codeswhat\.com/); assert.match(csp, /connect-src[^;]*https:\/\/e\.codeswhat\.com/); assert.doesNotMatch(csp, /\*\.posthog\.com|https:\/\/(app|us)\.posthog\.com/); - assert.doesNotMatch(body, /query-secret=should-not-leave/); + assert.match( + response.headers.get("cache-control") ?? "", + /\b(private|no-store)\b/, + "HTML reflects request data (Next.js flight payload), which is only safe if the page isn't shared-cached", + ); + // Next.js inlines router state, including the request's own query, in + // self.__next_f scripts. Everything else must not reflect the query. + const withoutFlightData = body.replace(/]*>([\s\S]*?)<\/script>/g, (block, inner) => + inner.includes("self.__next_f") ? "" : block, + ); + assert.doesNotMatch(withoutFlightData, /query-secret/); assert.match(body, /]*\bid="theme-init"/); }); diff --git a/frontend/test/posthog.test.ts b/frontend/test/posthog.test.ts index ff72703..13c5392 100644 --- a/frontend/test/posthog.test.ts +++ b/frontend/test/posthog.test.ts @@ -59,12 +59,20 @@ import { } from "../lib/posthog-privacy.ts"; const require = createRequire(import.meta.url); -const { PostHog } = require("../node_modules/posthog-js/lib/src/posthog-core.js") as { +// Deep import on purpose: the published main bundle minifies `_runBeforeSend` +// away, while lib/* is a published file in posthog-js's package.json `files` +// with no exports map. +const { PostHog } = require("posthog-js/lib/src/posthog-core.js") as { PostHog: new () => { config: { before_send?: (input: unknown) => unknown }; _runBeforeSend: (input: unknown) => unknown; }; }; +assert.equal( + typeof new PostHog()._runBeforeSend, + "function", + "posthog-js/lib/src/posthog-core.js no longer exposes PostHog#_runBeforeSend; the before_send pipeline contracts below need updating for this posthog-js version", +); // posthog-js attaches these to every envelope by default (PostHog/posthog-js // packages/browser-common/src/utils/event-utils.ts, getEventProperties()). From 2801dc4f2710a80f8a358971d8413c29275c936a Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:42:01 -0400 Subject: [PATCH 3/3] test(analytics): require real Flight scripts before excluding them Only scripts whose body starts with Next's self.__next_f push call count as Flight data, and at least one must exist, so a stray script containing the literal can't hide a reflected query. --- frontend/test/posthog-browser.test.mjs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/test/posthog-browser.test.mjs b/frontend/test/posthog-browser.test.mjs index 471ece3..28d18bf 100644 --- a/frontend/test/posthog-browser.test.mjs +++ b/frontend/test/posthog-browser.test.mjs @@ -20,9 +20,19 @@ test("production browser response keeps PostHog on the exact proxy", { "HTML reflects request data (Next.js flight payload), which is only safe if the page isn't shared-cached", ); // Next.js inlines router state, including the request's own query, in - // self.__next_f scripts. Everything else must not reflect the query. - const withoutFlightData = body.replace(/]*>([\s\S]*?)<\/script>/g, (block, inner) => - inner.includes("self.__next_f") ? "" : block, + // self.__next_f.push(...) scripts. Only scripts whose body starts with that + // call are excluded; everything else must not reflect the query. + const flightCall = + /^\(self\.__next_f\s*=\s*self\.__next_f\s*\|\|\s*\[\]\)\.push\(|^self\.__next_f\.push\(/; + let flightScripts = 0; + const withoutFlightData = body.replace(/]*>([\s\S]*?)<\/script>/g, (block, inner) => { + if (!flightCall.test(inner.trim())) return block; + flightScripts += 1; + return ""; + }); + assert.ok( + flightScripts > 0, + "expected Next.js Flight scripts in the HTML; the reflection exclusion is only valid when they exist", ); assert.doesNotMatch(withoutFlightData, /query-secret/); assert.match(body, /]*\bid="theme-init"/);