Skip to content

security: Enforce the Content-Security-Policy header - #15

Closed
TheCodeDestroyer wants to merge 5 commits into
mainfrom
csp-enforce
Closed

security: Enforce the Content-Security-Policy header#15
TheCodeDestroyer wants to merge 5 commits into
mainfrom
csp-enforce

Conversation

@TheCodeDestroyer

Copy link
Copy Markdown
Owner

Closes beads issue thecodedestroyer_github_io-35m.26.

What

  • Rename Content-Security-Policy-Report-OnlyContent-Security-Policy in next.config.ts.
  • Add upgrade-insecure-requests (browsers ignore it in report-only mode, so it only starts doing anything now).
  • Keep X-Frame-Options: DENY deliberately as zero-cost belt-and-braces beside the now-enforced frame-ancestors 'none'.
  • Rewrite the header doc comment: what was verified, how to re-verify after any policy change, and why next dev's single React-dev-only eval() console.error must not be silenced with 'unsafe-eval'.

Verification (local production build, headless Chrome)

Checked with the report-only header first and then again with the enforcing one — identical, clean results both times:

  • Zero CSP violations on first paint, scrolling every section, motion entrance animations, hovering all 10 halo cards (--mouse-x/--mouse-y inline writes) and toggling the mobile nav (desktop 1440×900 and mobile 390×844).
  • JSON-LD parses as a Person; /_next/image negotiates avif (10.6 KB), webp (21 KB) and png fallback; manifest.webmanifest plus all six icon routes return 200.
  • next dev (via localhost): hydrates, HMR connects, nothing blocked — only React's dev-build eval() console.error, which React never triggers in production.
  • pnpm lint and pnpm test (Playwright, 5/5) pass against the enforcing build.

The Vercel preview of this branch is used for the on-Vercel checks (/_vercel/insights and /_vercel/speed-insights script + beacons), which do not exist off-Vercel.

Note on the diff

This branch is based on the four local main commits that had not been pushed yet (b9e1322..122507e), so the PR shows them until main is pushed; the CSP change itself is the single commit f2da3c7.

🤖 Generated with Claude Code

TheCodeDestroyer and others added 5 commits August 27, 2026 17:06
- Add Spartan Development d.o.o. (Full Stack Developer, Sep 2026) as the
  current entry at the top of the career carousel
- Set the Optiweb tenure end date to Aug 2026
- Update the llms.txt intro to name Spartan as the current employer; the
  experience listing is generated from the same constant

Reflects the employer change effective 1 Sep 2026. Spartan bullets are
written in present tense as the active role and intentionally omit the
CTO track, which is planned for a Q1 2027 update to this same card.
Closes 22 of the 23 children of thecodedestroyer_github_io-35m. Only the
test-harness issue (.23) is left open.

Security (P0)
- next + eslint-config-next 16.2.9 -> 16.3.3, clearing 4 high and 5 moderate
  advisories (middleware/proxy bypass, SSRF in Server Actions and rewrites,
  DoS in Server Actions and image optimization, cache confusion, unbounded
  Edge Server Action payload, Server Function endpoint disclosure).
- postcss 8.5.16 -> 8.5.26 as a direct bump rather than an override, which
  also lifts nanoid transitively.
- pnpm.overrides collapsed from 17 entries to 4. Every deleted entry was
  proven dead with `pnpm why`; the two security entries that remain are
  load-bearing (brace-expansion via rimraf>glob, minimatch via
  eslint-plugin-import-x). pnpm audit is now clean for both --prod and dev,
  down from 23 advisories.

Bugs (P1)
- scroll-smooth moved from <body> to the actual scroll container in page.tsx,
  so navbar anchors ease instead of hard-jumping.
- Dropped react-device-detect. It read navigator.userAgent at module-eval
  time, so isMobile was always false during SSR and every section hydrated
  with a mismatch. The mobile opt-out is now a CSS media query, which the
  server and client agree on by construction.
- useLayoutEffect -> useEffect in SectionWrapper (SSR'd client component).
- "Contact me" was a <button> assigning window.location.href. It is now a real
  anchor at all three call sites, so it is crawlable, announced as a link, and
  supports middle-click/cmd-click/copy-link. contact.util.ts deleted.

CI (P1)
- checkout@v4->v7, setup-node@v4->v7, pnpm/action-setup@v2->v6; the hand-rolled
  store cache replaced with setup-node's native cache: 'pnpm'.
- Added --frozen-lockfile, a build step, a concurrency group, and a
  pull_request trigger; push now scoped to main. Node 22 -> 24.

Deprecations and a11y (P2)
- Disclosure.Button/.Panel -> DisclosureButton/DisclosurePanel.
- framer-motion 12 -> motion 13 (the package was renamed).
- prefers-reduced-motion is now respected across the section entrance
  animation, the border-spin ring, the halo, and anchor scrolling.
- Security response headers added. The CSP ships report-only on purpose; see
  -35m.26 for what must be verified on a preview before enforcing it.

Cleanup and additions (P3)
- Custom 404 page, web manifest, icon set, explicit robots, sitemap lastmod.
- Static imports replace five no-op next/dynamic wrappers in a Server
  Component, removing a request round-trip.
- Removed zod, stale tsconfig path aliases, dead CSS tokens, a no-op clsx, a
  redundant zustand spread, and an unused image quality.

Notes
- pnpm 10.25 defaults minimumReleaseAge to 1209600, which it reads as MINUTES
  (~840 days), blocking all resolution. Worked around per-invocation with
  --config.minimumReleaseAge=0; no config file was changed. See -35m.25.
- pnpm-lock.yaml is Prettier-formatted and lint-checked. Both conventions are
  now documented in the README.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes thecodedestroyer_github_io-35m.23, the last child of the maintenance
sweep epic. The repo had zero tests; CI ran eslint, prettier and tsc only.

Five tests, ~0.8s, always against the production build rather than `next dev` —
the section ids, metadata routes and prerendered markup are all build-time
output, so the dev server would be testing a different artifact.

- smoke: `/` returns 200, the h1 renders, and every value of the `Sections`
  enum is attached as an element id. Sourced from the enum, not a literal
  list, so a renamed section fails here instead of silently breaking deep
  links.
- metadata routes: /llms.txt, /robots.txt and /sitemap.xml return 200 with the
  expected content type AND a per-entry body marker. Status and content type
  alone are not enough — an empty sitemap generator still emits a well-formed
  `<urlset></urlset>`, which is the regression most worth catching.
- navbar highlight: asserts `aria-current` before and after scrolling to
  #technologies. Deliberately not asserting the `text-accent` class: both come
  from the same `isCurrent` ternary, so the class adds no signal and would go
  red on any Tailwind token rename.

Playwright over Cypress, despite Cypress being on the site's own skill list:
the ESLint preset sets `no-unused-expressions: error`, which bans the Chai
property assertions Cypress leans on, and Cypress's injected globals need a
second tsconfig. Playwright needed no eslint.config.mjs override at all, has
`webServer` built in (Cypress would need start-server-and-test as a second
dependency), and exposes `reducedMotion` as a first-class context option.

Each test was verified to fail when its subject breaks, not just to pass:
pinning the navbar highlight to the first section fails the post-scroll
assertion while the load-state one still passes, which is what proves the
scroll half is not a no-op.

CI: the tests run in the existing job after the build, since `next start`
needs that artifact. Splitting would mean uploading .next plus a second
checkout and install for no parallelism. Job renamed lint -> ci to match what
it now does. The report artifact upload is scoped to the E2E step so it does
not warn on lint or build failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename Content-Security-Policy-Report-Only to Content-Security-Policy now that the policy has been verified. Verified against the production build (next start) in headless Chrome, desktop and mobile viewports: zero violations on first paint, while scrolling every section, during the motion entrance animations, hovering the halo cards (--mouse-x/--mouse-y inline writes) and toggling the mobile nav; JSON-LD parses as a Person; /_next/image negotiates avif and webp; manifest.webmanifest and all icon routes load. Playwright suite passes against the enforcing build.

Also add upgrade-insecure-requests, which browsers ignore in report-only mode, and keep X-Frame-Options: DENY deliberately as zero-cost belt-and-braces now that frame-ancestors 'none' is enforced.

next dev under the enforcing header logs one React-dev-only eval() console.error but hydrates, connects HMR and blocks nothing, so no dev gating and no 'unsafe-eval'. No report-to sink, on purpose: re-verify by hand on a Vercel preview after any policy change.

Closes thecodedestroyer_github_io-35m.26

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thecodedestroyer Ready Ready Preview Aug 28, 2026 6:45am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant