feat(landing): precompile JSX so CSP can drop 'unsafe-eval'#47
Merged
Conversation
Closes audit #13. Replaces Babel-standalone in the browser with a precompiled bundle so the production CSP can be tightened from: script-src: 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://unpkg.com to: script-src: 'self' https://static.cloudflareinsights.com That removes the eval class of attack surface entirely (Babel was compiling JSX in the browser via `new Function()`, which required 'unsafe-eval'), and drops the unpkg.com supply-chain dependency since React + ReactDOM are now bundled. ## Approach Zero source-file changes. The 8 .jsx files in landing/ still use the familiar `React.useState` / `ReactDOM.createRoot` global API; nothing was refactored to ESM imports. Instead a tiny `scripts/build-landing.mjs` concatenates the files in their existing load order, prepends a shim that imports React + ReactDOM, and feeds the result through esbuild. This minimises the diff (the .jsx code is untouched), keeps the dev workflow understandable (each component is still a self-contained .jsx file), and gives us a clean production artifact (landing/dist/app.js, ~185 KiB minified, ~438 KiB sourcemap). ## Files - `scripts/build-landing.mjs` -- new, ~80 lines, esbuild bundle of the concatenated .jsx with classic JSX transform - `package.json` -- adds react@18.3.1 + react-dom@18.3.1 as runtime deps, @types/react + @types/react-dom + esbuild as dev deps, a `build:landing` script, and chains it into `build` - `Dockerfile` -- build stage now copies `landing/` and the build script, runs `pnpm build` (both backend tsup AND landing esbuild), production stage pulls landing/ (including dist/) from the build stage instead of the host - `landing/index.html` -- drops 3 unpkg.com `<script>` tags + 8 `<script type="text/babel">` tags; adds a single `<script src="/dist/app.js" defer>` - `src/server.ts` -- removes 'unsafe-eval', 'unsafe-inline', and unpkg.com from CSP script-src. style-src keeps 'unsafe-inline' for React's inline `style={{...}}` pattern and the inline `<style>` block (could be tightened in a future PR by extracting CSS, but the React-inline pattern is pervasive and not security-critical) - `tests/integration/landing.test.ts` -- replaces two stale tests (which checked individual .jsx routes) with three new ones that verify the new bundle is referenced, Babel/unpkg are absent, and the CSP header is strict `.gitignore` already excludes `landing/dist/` via the global `dist/` rule. landing/dist/ is generated only by `pnpm build:landing`; the production image gets it from the build stage where it's materialised. ## Local verification - `pnpm build:landing` -- 184.8 KiB bundle in 63ms - `pnpm typecheck` / `pnpm lint` / `pnpm test` -- 34 files / 460 tests - `node --check landing/dist/app.js` -- syntax OK - Docker image built side-by-side with prod, started clean, /health healthy, /dist/app.js served with `application/javascript`, CSP header confirmed strict (no unsafe-eval, no unpkg) Production container was NOT touched during validation. ## What's NOT in this PR - Inline `<style>` block in landing/index.html is unchanged. Removing it would let us drop 'unsafe-inline' from style-src too, but it's a bigger CSS refactor (the file has heavy inline-style usage and React inline styles still trigger style-src). Out of scope here. - The 8 .jsx files keep using `React.useState` etc. globally. A future cleanup could convert them to ESM modules with named hook imports for better tooling support (autocomplete, jump-to-def). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes audit #13. Replaces Babel-standalone in the browser with a precompiled bundle so the production CSP can be tightened from `script-src: 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://unpkg.com\` to `script-src: 'self' https://static.cloudflareinsights.com\`.
That kills the eval class of attack surface (Babel was compiling JSX in the browser via `new Function()`) and drops the unpkg.com supply-chain dependency since React + ReactDOM are now bundled into our own artifact.
Approach (and why this is small, not a 1-week rewrite)
Zero source-file changes to landing/*.jsx. The 8 files still use the familiar `React.useState` / `ReactDOM.createRoot` global API; nothing was refactored to ESM imports. Instead a small `scripts/build-landing.mjs` concatenates the files in their existing load order, prepends a shim that imports React + ReactDOM into module scope, and feeds the result through esbuild.
Files
What's intentionally NOT in scope
<style>block in landing. Removing it would let us drop `'unsafe-inline'` from `style-src` too, but it's a bigger CSS refactor (the file has heavy inline-style usage AND React inline styles still trigger style-src). Future PR.Local verification I ran before this PR
Production deploy
This needs your usual `bash deploy.sh` on the VPS. The deploy will:
Visual regression risk: the React rendering pipeline is the same (classic JSX transform, same React version) but you should load `https://cardano402.com\` after deploy and:
If anything looks wrong, the rollback recipe in `docs/operations.md` works as before.
🤖 Generated with Claude Code