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 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..28d18bf 100644 --- a/frontend/test/posthog-browser.test.mjs +++ b/frontend/test/posthog-browser.test.mjs @@ -14,6 +14,26 @@ 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.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"/); }); 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()).