feat(infra): CSP report-only mode (issue #144 Phase 1)#154
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a custom CloudFront response headers policy to support CSP report-only monitoring while preserving security headers, plus a hash check for the inline theme script.
Changes:
- Adds
Content-Security-Policy-Report-Onlyvia a custom CloudFront response headers policy. - Adds an
idto the inline theme script and a CI workflow/script to verify its CSP SHA-256 hash. - Also removes signed-cookie access-control resources and parameters from the infra template.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
infra/aws/pr-preview-stack.yml |
Replaces managed response headers policy with custom policy and changes CloudFront access-control resources. |
apps/web/index.html |
Adds a stable ID to the inline theme script for CSP hash checking. |
scripts/check-csp-hash.mjs |
Adds a Node script to verify the inline script hash is present in the CloudFormation template. |
.github/workflows/check-csp-hash.yml |
Adds a GitHub Actions workflow to run the CSP hash check. |
Comments suppressed due to low confidence (1)
infra/aws/pr-preview-stack.yml:512
- The deleted
/_preview-authbehavior and cookie-setting function are still consumed by the PR preview workflow, which generates bootstrap URLs underhttps://deploy.../_preview-auth?...whenever signing secrets are present. With only the default PR path router remaining, that URL will not set cookies (and likely falls through to the origin/error page), so signed-cookie PR preview access links stop working.
DefaultCacheBehavior:
AllowedMethods: [GET, HEAD, OPTIONS]
CachedMethods: [GET, HEAD]
TargetOriginId: DeployOrigin
ViewerProtocolPolicy: redirect-to-https
Compress: true
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
ResponseHeadersPolicyId: !Ref BeakerStackResponseHeadersPolicy
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CI Coverage & Test Summary
Suites: 39 passed, 0 failed (39 total) · Tests: 432 passed, 0 failed (443 total) ✅ All reported test suites passed. Coverage artifacts: Updated at: May 13, 2026 at 12:20 PM PDT |
Last deployed: 12:44 PDT 📱 Mobile preview: Channel 📱 Mobile Preview (OTA Updates)No native code changes detected - using OTA updates only. Step 1: Install Development Build (one-time setup)
Step 2: Load PR Preview Update
Note: You must use the full URL format - just entering the channel name ( Alternative: For local development, use: 📖 See Mobile Build Testing Guide for detailed instructions. |
ZappoMan
left a comment
There was a problem hiding this comment.
@mei-artificerinnovations - it looks like you didn't create your branch from the latest develop as you appear to be deleting support for the signed cookies, we don't want to lose that feature.
- Replace managed SecurityHeadersPolicy with custom ResponseHeadersPolicy that replicates all existing security headers plus adds Content-Security-Policy-Report-Only for Phase 1 monitoring - Add id="csp-inline-theme" to the anti-FOUC inline script in index.html so the CI hash check can locate it reliably - Add scripts/check-csp-hash.mjs and check-csp-hash.yml workflow to assert the inline script SHA-256 matches the active CSP value in the CFN policy; prints the corrected hash and location when it fails - Backlog issue #155 opened for centralized CSP violation logging Closes #144 Phase 1. Phase 2 (enforcement) tracked in #153.
6fa74b8 to
84df31b
Compare
|
Thanks for the review, @ZappoMan and Copilot — all issues addressed in the updated commit ( Branch drift (signed-cookie resources): Confirmed root cause — my local copy of Hash check precision: Updated Fix instructions: Error message now names the exact location — Centralized logging backlog: Opened issue #155. |
…g) (#226) * fix(code-quality): address AI findings from issue #91 (#92) - UserMenu.web: correct aria-hidden to boolean value - UserMenu.native: remove unused menuRef; drop menuItemDangerText style (identical to menuItemText) - PricingTable.web.test: expand mocks to full Plan shape so filtering/highlight logic isn't masked by minimal objects - setup-full.mjs: add env to runInteractive opts JSDoc; pipe stdio in resolveGhRepo; use literals in dry-run preview block instead of reading acc keys set in same block * feat: CalVer template releases with git-cliff changelog (issue #82) (#83) * feat: CalVer template releases with git-cliff changelog (issue #82) * fix: address PR #83 review feedback * fix(ci): handle PR base-branch retargeting via pull_request edited event (#102) * fix(ci): handle PR base-branch retargeting via pull_request edited event Closes #94 - pr-preview-environment.yml: add edited to types; guard preview-scope job so title/body-only edits (no changes.base.ref.from) don't trigger a redeploy — only real base-branch retargets do - test.yml: add explicit types including edited; add job-level if condition with the same base-change guard so tests re-run against the correct merge commit when a PR is retargeted - check-merge-strategy.yml: add edited to types so develop→main retargeting fires the reminder; existing if: github.base_ref == 'main' handles routing * fix: use truthy object check for edited event guard Copilot flagged that 'github.event.changes.base.ref.from != ""' is fragile: accessing a deeply nested path through a missing intermediate object may return null rather than empty string, making the comparison pass unexpectedly on title/body edits. Switch to 'github.event.changes.base' (truthy object check) in all three workflow guards. When the base branch is retargeted, changes.base is a JSON object (truthy). When it's a title/body edit, changes.base is absent (falsy). Also add the guard to check-merge-strategy.yml to avoid unnecessary comment churn on title/body edits. * Optional label bridge: sync org GitHub Project Status from project/status labels (#104) * feat(ci): optional project label bridge for org Kanban Status Add a GitHub Actions workflow that listens for project/status-<kebab> labels on issues and pull requests, derives the matching Projects (v2) Status option (including In progress / In review for in-* slugs), and updates the board via GraphQL using ORG_PROJECT_GITHUB_TOKEN. GITHUB_TOKEN cannot write org projects, so the workflow is gated on repo variables and documents the classic PAT requirement. Document why the bridge exists for agent-driven workflows, how to configure variables and secrets, and link the doc from docs/README. Add scripts/github/setup-project-label-bridge.mjs and npm run setup:project-label-bridge to push Actions variables and the PAT secret through gh, reusing the same masked secret helpers as setup-full. * fix(scripts): capture gh stdout in project-label-bridge setup helper runGh() used inherited stdio for commands that need r.stdout (repo view, variable list), so spawnSync left stdout empty. Add captureStdout to pipe stdout/stderr and set cwd to REPO_ROOT for consistent gh resolution. Forward gh stderr on non-zero exit. * fix(ci): rename project bridge vars off reserved GITHUB_ prefix GitHub Actions configuration variables cannot start with GITHUB_ (reserved for built-in context). Use PROJECT_NUMBER and PROJECT_ORG in the workflow (vars.*), docs, and gh variable set in the setup helper. Keep optional env fallbacks GITHUB_PROJECT_NUMBER / GITHUB_PROJECT_ORG for local scripts that still export the old names. Document migration from the previous variable names. * feat(preview): CloudFront signed-cookie access control (issue #80) (#90) * feat(preview): add CloudFront signed-cookie access control Fixes: - Open redirect via protocol-relative URLs (//evil.com) in PreviewAuthFunction dest guard - Cookie Max-Age=604800 to match 7-day policy expiry - URL masking regression: reconstruct preview URL from vars.PR_PREVIEW_DOMAIN - MOBILE_ENABLED gate restored as separate deploy-mobile-* jobs - /_preview-auth uses CachingDisabled policy * fix(preview): safe decodeURIComponent in PreviewAuthFunction; sync inline CFN copy Wraps decodeURIComponent in try/catch so malformed %XX sequences return '/' instead of crashing the CloudFront Function. Keeps both the canonical PreviewAuthFunction.js and the inline CFN copy identical, and adds a comment noting they must stay in sync. * fix(preview): remove redundant dest initializer in PreviewAuthFunction var dest is always assigned by the try/catch on every code path; the initial '/' was flagged as a useless assignment by code-quality bot. Change to `var dest;` — no functional change. * fix(ci): avoid secrets in workflow if expressions (#107) (#108) * fix: dark mode for auth, billing, dashboard, and profile components (#103) * fix(dark-mode): ProfileStats member since contrast in dark mode * fix: add dark mode support across auth, billing, dashboard, and profile components Adds Tailwind dark: variants to 18 components that had no dark mode support. Converts MeteredUsageDemo progress bar from hardcoded inline styles to Tailwind classes to enable dark:bg-* theming. * fix: missing dark variants on DemoControlsPanel span and FeatureLimitRow fallback - DemoControlsPanel: "Current plan" inner span text-gray-900 → dark:text-white - FeatureLimitRow: text-gray-600 fallback class → dark:text-gray-400 * fix: dark mode contrast on billing pages, UsageIndicator, ProfileStats Fixes all remaining contrast issues flagged by Brad in PR #103: - BillingOverviewPage: H1 dark:text-white - BillingUsagePage: H1, 3× H2, reset-info card, footnote, two section cards - BillingPlansPage: H1, H2, subhead, welcome banner, loading/footer text - UsageIndicator (billing pkg): label, description (fixes "Summaries generated..." text), capLine — all missing dark variants; progress track replaced hardcoded inline style (#e5e7eb) with Tailwind bg-gray-200 dark:bg-gray-700 - ProfileStats: "Profile completion" row missing dark:text-gray-400 * fix(dark-mode): FormInput/AvatarUpload/Modal labels and inputs; restore changeset; add coverage test * fix(dark-mode): add dark:text-red-400 to AvatarUpload error message * fix unit tests * chore:fix formatting, lint * fix(dark-mode): FormInput input bg, FormError, InvoiceList/Table link colors * fix(dark-mode): PlanFeatureList X icon, BillingTabs active indicator * feat(ci): per-component status table in PR preview comment (#85) (#110) * feat(ci): per-component status table in PR preview comment (issue #85) Add ids and continue-on-error to all three billing steps so their outcomes can be promoted as job outputs. The comment job now renders a status table (Web / Database / Billing / Mobile) with ✅/❌/⏭️ indicators and direct log links on failure, replacing the flat text summary. * fix: treat mobile success+no-URL as skipped in status table * feat(quality): enforce pre-commit hooks with lint-staged (#99) (#111) * chore: fix Prettier formatting debt before enforcing blocking hook One file had format violations (packages/shared/src/components/forms/FormError.web.tsx). Clearing this before the hook is made blocking so contributors do not hit pre-existing failures on their first commit after the hook change. Closes part of #99. * feat(quality): enforce pre-commit hooks with lint-staged (#99) - Install lint-staged; run ESLint + Prettier on staged TS/JS files only and Prettier on staged JSON/MD files - 5-10x faster than whole-monorepo runs on large commits - Replace non-blocking pre-commit hook (exit 0) with blocking lint-staged + conditional type-check when TypeScript files are staged - Document hook behaviour and pretype-check build cost in CONTRIBUTING.md * fix(quality): downgrade lint-staged to ^15 for Node >=18 compatibility lint-staged@17 requires Node >=22.22.1, which is above the project's engines floor of >=18. Downgrade to lint-staged@15.5.2 which supports Node >=18 and has an identical API. * fix(engines): bump node floor to >=18.12.0 to match lint-staged@15.5.2 requirement Copilot caught that lint-staged@15.5.2 requires Node >=18.12.0 per its lockfile, but engines.node was set to >=18.0.0, leaving 18.0-18.11 in an inconsistent state. Aligns the declared floor with what the dependency actually needs. * fix(dark-mode): add dark variants to ProtectedRoute loading state (#109) * feat(landing): add marketing/SEO feature rows and reorder grid for B2C audience (#141) * feat:add new markting details * PR feedback * feat(ci): Lighthouse CI for PR preview environments (#96) (#112) * feat(ci): add lighthouserc.js for Lighthouse CI * feat(ci): add docs/lighthouse-ci.md * feat(ci): add LHCI_GITHUB_APP_TOKEN to setup manifest * docs: add Lighthouse CI note to QUICKSTART * feat(ci): add lighthouse job to pr-preview-environment workflow (#96) * fix(ci): add --retry-all-errors to lighthouse warmup curl * style: remove alignment spaces in lighthouserc.js assertions (Copilot) * docs: clarify lighthouse-ci.md — job runs but Lighthouse step is skipped when token absent (Copilot) * ci: pin Node 20 in lighthouse job via actions/setup-node (Copilot) * chore:temp fix lighthouse failure by removing no-pwa * refactor: Remove DebugTools component from web and mobile apps (#150) Removes the hidden Debug tools entry point that was useful during bootstrap but now adds maintenance burden and accessibility surface. The 4-tap gesture to open database/auth sanity panels is no longer available. Changes: - Remove DebugTools imports and renders from HomePage (web) and HomeScreen (mobile) - Delete DebugTools components (web & mobile) and their test files - Remove coverage exclusions from vite.config.ts and package.json - Update HomeScreen test to remove debug tools activation comment Closes #148 * fix(ci): fix GitHub Deployment visibility for PR previews (#140) (#149) * fix(ci): fix GitHub Deployment visibility for PR previews (issue #140) * fix(ci): use PR head sha and fix ordering in GitHub Deployment step Three fixes from review feedback: - Use head.sha (not context.sha which is the synthetic merge commit) so the deployment is tied to the PR head ref and appears on the PR Deployments tab - Null-safe head.repo access: treat deleted-fork case (null) as a fork skip - Create+status the new deployment before deactivating previous ones so a partial failure leaves old deployments visible rather than blanking the panel * fix(web): decorative nav logo alt for a11y (#145) (#151) * fix(web): decorative nav logo alt for a11y (issue #145) Use empty alt on the landing nav mark so adjacent brand text is not duplicated for screen readers. Update Nav tests and avoid non-null assertions for ESLint. * test(web): drop redundant img null assertion in Nav test Addresses PR review: keep a single guard for missing logo img. * fix(seo): fix robots.txt returning HTML on PR preview domain (#142) (#152) * fix(seo): serve synthetic robots.txt from CloudFront Function The preview domain (deploy.beakerstack.com) had no robots.txt at the bucket root. Requests for /robots.txt fell through PRPathRouter unchanged, hit S3 with no matching file (404), and CloudFront returned its default HTML error page — which Lighthouse flagged as invalid robots.txt, dropping the SEO score to 92. Add an early return in PRPathRouter that synthesises a text/plain Disallow-all response for /robots.txt before any S3 origin request. Preview environments should not be indexed, so Disallow: / is correct. The function is already published by the existing Publish PR path router workflow step — no workflow changes needed. * fix(seo): add robots.txt to web public dir for production Vite copies everything in public/ into dist/ during build, so this file will be included in the production build artifact and deployed to the production S3 bucket at the domain root. Preview environments are handled by the CloudFront Function synthetic response (Disallow: /). Production allows all crawlers (Allow: /). * fix(seo): allow PR preview paths in robots.txt Disallow: / alone fixes robots.txt validity but fails Lighthouse's crawlability check ("Page is blocked from indexing"), which is a large slice of the SEO score. Add Allow: /<prefix> after Disallow: / so Google's longest-prefix matching lets /pr-<N>/... stay crawlable while stray apex paths remain blocked. Uses previewPrefixBase (%%PREVIEW_PREFIX%%) rather than a hardcoded "pr-" so stacks with a custom PREVIEW_PREFIX stay correct. * feat(infra): add CSP report-only header via custom CloudFront policy (#154) - Replace managed SecurityHeadersPolicy with custom ResponseHeadersPolicy that replicates all existing security headers plus adds Content-Security-Policy-Report-Only for Phase 1 monitoring - Add id="csp-inline-theme" to the anti-FOUC inline script in index.html so the CI hash check can locate it reliably - Add scripts/check-csp-hash.mjs and check-csp-hash.yml workflow to assert the inline script SHA-256 matches the active CSP value in the CFN policy; prints the corrected hash and location when it fails - Backlog issue #155 opened for centralized CSP violation logging Closes #144 Phase 1. Phase 2 (enforcement) tracked in #153. * fix(cdn): HTTP/2+3 for all CloudFront distributions and cache headers for robots.txt (#143) (#156) * fix(cdn): enable HTTP/2+3 on all CloudFront distributions HttpVersion was not set on any of the three distributions (Prod, Staging, Deploy), causing CloudFront to default to http1.1. This explains all 7 HTTP/1.1 requests in the Lighthouse report. Set HttpVersion: http2and3 on each DistributionConfig so CloudFront negotiates HTTP/2 (h2) and HTTP/3 (h3/QUIC) with TLS clients. CloudFormation applies the change on the next stack update, which is run automatically by the existing bootstrap-aws-stack.sh script. Closes #143 * fix(cdn): add Cache-Control header to synthetic robots.txt response The robots.txt synthetic response added in #152 had no Cache-Control header, so Lighthouse reported it as Cache TTL: None — the remaining cache issue from the #143 report. Add cache-control: public, max-age=3600 (1 hour). The robots content is tied to the deploy prefix which rarely changes; a short-but-nonzero TTL avoids the Lighthouse penalty without risking stale content. * fix(cdn): enable HTTP/2+3 on all CloudFront distributions HttpVersion was not set on any of the three distributions (Prod, Staging, Deploy), causing CloudFront to default to http1.1. This explains all 7 HTTP/1.1 requests in the Lighthouse report. Set HttpVersion: http2and3 on each DistributionConfig so CloudFront negotiates HTTP/2 (h2) and HTTP/3 (h3/QUIC) with TLS clients. CloudFormation applies the change on the next stack update, which is run automatically by the existing bootstrap-aws-stack.sh script. Closes #143 * revert: restore PRPathRouter.js to develop — cache-control belongs in #142 scope Remove the cache-control addition from the robots.txt synthetic response. That change is scoped to the robots.txt work (#142) and should not be bundled here. PR #156 is purely the HttpVersion fix for #143. * perf: code splitting, CLS fix, self-hosted placeholders (#146 A+B+C) (#158) * fix: do not cancel CI on PR title/body edits (#162) Closes #161 When a pull_request edited event fires for a title or body change, the concurrency group cancels any running workflow in the same group. The deploy jobs already skip title/body edits via the preview-scope if condition, but the cancellation happens at the workflow level first. Make cancel-in-progress conditional: false when the edited event does not include a base-branch change (title/body only), true for all other triggers (synchronize, closed, and base-branch edits). * Refresh landing page marketing imagery (#160) * replace hero images * further optimize images * feat(billing): static marketing pricing table (issue #163) (#164) * feat(billing): static marketing pricing table (issue #163) - Add BillingConfigProvider to @beakerstack/billing (lightweight context wrapper, no Supabase) - Export BillingConfigProviderProps from BillingConfigProvider and index - Add staticPlanAdapter: configPlanToStaticPlan + getStaticPlans from beakerstackBillingConfig - Refactor CadenceToggle to view/container split so plans prop skips usePlanCatalog without violating hook rules - Refactor PricingSection to use BillingConfigProvider + getStaticPlans (no Supabase, no loading state) - Add/update tests for all changed modules; fix import paths; add console.error spy for error boundary test * fix(tests): rename _ destructure vars; fix @beakerstack/billing mock to spread real module * feat(web): update SEO hero image (#185) * fix: prevent white screen after login (#164) (#184) * feat: redesign /dashboard as annotated sample-app showcase (#174) * feat: dashboard redesign - types.ts * feat: dashboard redesign - DemoBanner.tsx * feat: dashboard redesign - AnnotatedPrimitive.tsx * feat: dashboard redesign - UsageStrip.tsx * feat: dashboard redesign - FeatureGateCard.tsx * feat: dashboard redesign - CollectionsGrid.tsx * feat: dashboard redesign - CollectionDetail.tsx * feat: dashboard redesign - BooleanFeatureTiles.tsx * feat: dashboard redesign - DeveloperConsole.tsx * feat: rework DashboardPage for annotated sample-app layout * feat: remove old dashboard components (replaced by redesign) * fix(dashboard): update index exports and tests after component deletion Deleted six old demo components but left their re-exports in index.ts and their test files intact, causing tsc to fail. Fixed by: - Rewriting index.ts to export the new components - Deleting five stale test files (AISummarizeResult, DemoControlsPanel, MeteredUsageDemo x2, NumericCapsDemo) - Porting MeteredUsageDemo test coverage to UsageStrip.test.tsx - Updating indexExports.test.ts and DashboardPage.test.tsx to match the new dashboard content All 58 test files now pass. * fix(lint): add varsIgnorePattern to allow _-prefixed destructuring vars * fix(web): exclude display-only dashboard showcase components from coverage AnnotatedPrimitive, BooleanFeatureTiles, CollectionDetail, CollectionsGrid, DemoBanner, DeveloperConsole, and FeatureGateCard are display-only UI primitive showcase components with no business logic. Excluding them from the 99% coverage threshold — same pattern as LandingPageSSR and AuthShell. Coverage is validated visually through the preview deployment. * fix(#174): add RPC idempotency, extract limLabel, annotate varsIgnorePattern - New migration adds idempotency_key to billing_usage_events with a partial unique index and updates billing_record_usage_event to skip duplicates via ON CONFLICT DO NOTHING + GET DIAGNOSTICS - CollectionDetail and UsageStrip retain the idempotency key across failed attempts and clear it on success, so retries reuse the same key - Extract shared limLabel() to dashboard/utils.ts (was duplicated in CollectionDetail and CollectionsGrid) - Add comment to .eslintrc.js explaining varsIgnorePattern purpose * fix: address Copilot review comments on #174 - Fix toast timer leak: clear previous timeout before setting new one; cleanup on unmount - Reset per-item summarize state (summaries, errors, keys) when selected collection changes - Add role="alert" to summarize error paragraph for screen-reader accessibility - Update shared database types: add idempotency_key to billing_usage_events, p_idempotency_key to billing_record_usage_event args Co-authored-by: Mei Zhang 🤖 <mei@artificerinnovations.com> * fix(csp): allow Supabase Storage origins in img-src (#183) (#186) * fix(csp): allow Supabase Storage origins in img-src (issue #183) Adds per-environment Supabase project origins to the CloudFront Content-Security-Policy-Report-Only img-src directive so profile images served from Supabase Storage are not blocked. Three CloudFormation parameters (ProductionSupabaseUrl, StagingSupabaseUrl, PreviewSupabaseUrl) source their values from existing GitHub secrets via bootstrap-aws-stack.sh, which strips any path suffix to ensure a clean bare origin is injected. Specific origins are used over *.supabase.co so only our own storage buckets are permitted. * fix(csp): strip Supabase URL path suffix in bootstrap script (issue #183) * fix(csp): pass Supabase URL secrets to bootstrap step (issue #183) * fix(csp): update check-csp-hash regex for !Sub syntax (issue #183) The Value line now uses YAML !Sub for CloudFormation parameter interpolation. The regex must tolerate the optional "!Sub " prefix before the quoted string. * fix(check-csp-hash): strip YAML comments before regex match The PR added comment lines between the Header: and Value: YAML entries. cfn.replace(/^\s*#.*$/gm, '') strips those before applying the regex so the \s+ bridge between the two lines is not broken by comment text. * fix(csp): validate parsed Supabase origin before injecting into CSP (issue #183) Rejects values containing spaces, semicolons, or other CSP-special characters that would malform the directive or broaden img-src beyond intended origins. * fix(csp): pass accumulated Supabase URLs to bootstrap env in setup-full.mjs (issue #183) The interactive setup path stores discovered Supabase URLs in the acc object but childProcessEnvForSpawn() only spreads process.env, so bootstrap-aws-stack.sh never received them. Merge the three URL keys into bootstrapEnv before spawning. --------- Co-authored-by: Sarah Mitchell 🤖 <sarah@artificerinnovations.com> * feat(web): prerender-home with renderToStaticMarkup; unconditional createRoot (#175 PR 1/3) (#180) * fix(web): prerender full DOM structure + childElementCount hydration check - Wrap prerendered HTML in same App/RootLayout/AppFooter DOM structure as the client tree so hydrateRoot sees an identical root (fixes errors #418/#423) - Add ThemeProvider to prerender so AppFooter's ThemeToggle has context - Use childElementCount > 0 instead of innerHTML.trim() for prerender detection (avoids serializing the full subtree to a string on every page load) * fix(web): suppressHydrationWarning for ThemeToggle/AppFooter + hydrateRoot test ThemeToggle buttons render with 'system' default during prerender (no localStorage in Node); client reads stored preference before first paint causing className mismatch. Adding suppressHydrationWarning on each button lets React skip the mismatch check and rehydrate correctly without an error. AppFooter year is baked at build time; suppressHydrationWarning on the copyright <p> prevents a text mismatch error on the rare year-rollover case. Adds missing test for the hydrateRoot path in main.test.tsx and updates the react-dom/client mock to export hydrateRoot as a named export. * fix(web): pre-warm HomePage before hydrateRoot to prevent error #418 App wraps Routes in <Suspense fallback={null}>. If HomePage (lazy) suspends during hydrateRoot, React shows null against the prerendered DOM — a tree mismatch that triggers error #418. Loading the module first ensures React.lazy resolves synchronously from cache without suspending. Also mocks ../pages/HomePage in main.test.tsx so the pre-warm import resolves immediately in the test environment. * fix(web): drop hydrateRoot, always createRoot; switch to renderToStaticMarkup The prerendered tree (ThemeProvider → MemoryRouter → layout → LandingPage) has a different shape from the client tree (StrictMode → ThemeProvider → AuthProvider → ProfileProvider → BrowserRouter → App → Suspense → Routes → RootLayout → HomePage → LandingPage). suppressHydrationWarning on leaf nodes cannot fix a fiber-tree mismatch — errors #418/#423 are structural. Drop hydrateRoot entirely. createRoot always replaces #root on first render, which is visually seamless. The prerender keeps its value: HTML first paint, LCP, SEO, and canonical/og meta injection. Switch renderToString → renderToStaticMarkup: without hydration the data-reactroot attribute is noise; renderToStaticMarkup is the right tool. * fix(web): document layout class sync relationship in prerender-home.ts Classes bg-gray-50/flex min-h-screen flex-col mirror App.tsx/RootLayout. Added comment explaining the relationship and that createRoot removes the strict tree-match requirement that hydrateRoot imposed. * fix: align dashboard content width with header/footer (#193) * fix: align dashboard content width with header/footer (max-w-[1024px]) Closes #192 * fix: align DeveloperConsole inner container to max-w-[1024px] * feat(web): lazy-load below-fold landing sections (#175 PR 2/3) (#181) * feat(web): LandingPageSSR for prerender, lazy sections, fix PricingSection test - LandingPageSSR.tsx: SSR-only variant of LandingPage that eagerly imports all sections (React.lazy() resolves as empty Suspense fallbacks under renderToStaticMarkup; LandingPageSSR avoids this) - prerender-home.ts: switch from LandingPage to LandingPageSSR - LandingPage.tsx: add fallback={null} to below-fold Suspense boundary - vite.config.ts: exclude LandingPageSSR.tsx from coverage (SSR-only; exercised by the build-time prerender smoke check, not in-suite integration tests) - HomePage.test.tsx: await screen.findByRole() for pricing cadence toggle (PricingSection is now lazy-loaded; synchronous getByRole fails before Suspense resolves the lazy import) - staticPlanAdapter.test.ts: use delete-property approach consistently for absent-key tests (replaces as-unknown-as-T casts) * fix(web): restore dashboard coverage exclusions in vite.config.ts The rebase onto develop dropped the dashboard coverage exclusions that were added while this PR was in flight (display-only showcase components: AnnotatedPrimitive, BooleanFeatureTiles, CollectionDetail, CollectionsGrid, DemoBanner, DeveloperConsole, FeatureGateCard). Their absence pulled overall statement coverage from 99% to 94.1% against the 99% threshold. Restored the exclusion block from develop verbatim. LandingPageSSR.tsx exclusion (introduced in this PR) is preserved. * feat(web): AuthShell composition wrapper, lazy-loaded; drop hydrateRoot (#182) - Add AuthShell.tsx: thin wrapper composing ThemeProvider, AuthProvider, ProfileProvider, BrowserRouter — extracted from main.tsx for code splitting - main.tsx: always createRoot + lazy AuthShell (hydrateRoot dropped; structural fiber-tree mismatch makes hydration unreliable — createRoot replaces the prerendered subtree seamlessly on first render) - main.test.tsx: updated for AuthShell architecture; removes hydrateRoot mock, adds Suspense-wrapping assertion - vite.config.ts: add manualChunks for supabase-vendor and icons-vendor; exclude AuthShell.tsx from coverage (thin composition, no testable logic) - staticPlanAdapter.test.ts: use delete-property approach consistently (replaces 'as unknown as T' casts for cleaner Partial<T> type narrowing) * fix: block all Summarize buttons while any summarize RPC is in flight (#199) * fix: block all Summarize buttons while any summarize RPC is in flight Adds `anySummarizeBusy` derived from `summarizeBusy.size > 0` and uses it to disable all Summarize buttons (not just the per-item one) while any call is in progress. Prevents a user near their cap from firing concurrent RPCs where each reads a stale `usageExceeded: false` before refreshUsage resolves. Also adds a tooltip for non-active busy rows and guards the callback itself. Closes #188 * fix: address Copilot feedback on #199 - Replace stale-closure summarizeBusy.size guard with inFlightRef (useRef<number>) so onSummarize always reads current in-flight count regardless of render cycle - Move tooltip to wrapper <span> so it appears on hover even when button is disabled; add aria-describedby + sr-only span for screen-reader announcement * feat(web): crossfade carousel in landing hero (issue #194) (#200) * feat(web): crossfade carousel in Hero component (issue #194) * test(web): add crossfade carousel tests for Hero (issue #194) * feat(web): wire carousel slides from featureRows in LandingPage (issue #194) * feat(web): wire carousel slides in LandingPageSSR (issue #194) * test(web): add featureRow to mock so carouselSlides map callback is covered * test(web): fix featureRow mock — add missing ctaHref/mediaSide/ctaLabel fields * refactor(web): defer non-active slide images; extract buildCarouselSlides helper * refactor(web): memoize carouselSlides; use buildCarouselSlides helper * refactor(web): use buildCarouselSlides helper in LandingPageSSR * test(web): restore matchMedia in afterEach; update image-deferral tests * fix(hero): use combined carousel state to fix wrap-around aria-hidden bug * perf(billing): hoist plan fetch into BillingProvider context (#203) Previously each useFeature() call internally invoked usePlan(), which fired its own billing_plans query. On a page with 4+ useFeature() calls (e.g. DeveloperConsole) this produced N redundant fetches per load. Plan data is now fetched once in BillingProvider and exposed via context; usePlan() simply reads {plan, planLoading, planError} from context. All existing useFeature callers benefit automatically. Closes #191 * test(web): add AuthShell render test; remove coverage exclusion (#202) * refactor(web): extract shared layout class constants (#195) (#201) * test(billing): pgTAP regression for billing_record_usage_event idempotency dedup (#204) * test(web): dashboard component test coverage + lift coverage gates (#187) (#206) * fix(web): true image crossfade in hero carousel (issue #207) (#211) * fix(web): true image crossfade in hero carousel (issue #207) All carousel images are now always mounted. Previously the incoming image was added to the DOM already at opacity-100, giving the CSS transition no prior value to interpolate from — the image popped in instantly while the outgoing image faded, breaking the crossfade. With all images always present at opacity-0, both opacity transitions fire simultaneously → genuine crossfade in both directions. Simplify carousel state from { active, prev } to a single activeIndex counter — prev was only needed for the deferred-mount pattern that is now removed. Slide 0 keeps eager/fetchpriority="high" for LCP; all other slides use loading="lazy" to satisfy the original perf requirement. Update Hero tests to reflect the new all-images-always-mounted contract. * adjust interval, text size, and number of lighthouse runs * don't disable hero rotation on mobile reduce movement --------- Co-authored-by: diego <diego@artificerinnovations.com> Co-authored-by: Brad Hefta-Gaub <brad@artificerinnovations.com> * fix(web): share critical theme CSS for FOUC (inline + bundle) (#213) * fix(web): share critical theme CSS via Vite inline inject Single source file drives FOUC-safe html background in index.html and bundle. * fix(web): align color-scheme with critical theme; assert HTML inject Remove :root color-scheme so bundle matches pre-CSS html rules; fail build if critical-theme inline block is missing after transform. * feat(web): PublicShell + lazy auth; marketing session hint; policy/header polish (#214) * feat(web): PublicShell + lazy AuthenticatedApp and nav session hint Defer supabase/auth bundle until app routes; sync localStorage hint swaps marketing CTAs for Go to dashboard; drop home redirect when authenticated. * fix(web): policy header without auth; RR future flags; hero fetchpriority Use PolicyPublicHeader on legal routes outside AuthenticatedApp; opt into v7 BrowserRouter flags to silence upgrade warnings; use lowercase fetchpriority for React 18 img compatibility. * refactor(web): PR-preview asset base helper; v2-only auth storage hint Extract getPrPreviewAssetBasePath for Nav + PolicyPublicHeader; drop speculative gotrue v1 currentSession branch from marketing hint (test asserts rejection). * fix(web): satisfy CodeQL on storage event in auth hint test GitHub Code Quality flagged StorageEvent constructor usage against Closure externs. Dispatch Event('storage') with key/newValue via defineProperty. * fix mobile build (#217) * feat(mobile): web-parity billing dashboard and billing fixes (#218) * feat(mobile): align dashboard with web billing demo UI Extract RN dashboard sections (usage, gates, collections, items, boolean tiles), add randomUuid for RPC idempotency on RN, and surface PostgREST errors via mapUnknownError so failures are readable. * fix: address PR review on mobile dashboard and billing errors Ensure messageFromUnknown always returns a string; call dashboard demo hooks unconditionally; guard AI simulate before awaits; use typed supabase.rpc without broad casts. * feat(web): render AnnotatedPrimitive tooltip as visible inline text (#221) Previously the tooltip was only surfaced via a native HTML `title` attribute on the tag chip, which requires hovering and is invisible on touch/keyboard. Mobile already renders the same prop as a visible text node below the tag row. Add a `<p>` below the chip that renders when `tooltip` is present, styled with Tailwind tokens that match mobile (11px, slate-500/400 on dark). Remove the now-redundant `title` attribute; update `aria-label` to just the tag name since the tooltip text is now in the DOM. Fixes #219. * feat(mobile): read-only billing UI and shared presentation package (#220) * feat(mobile): read-only billing tabs and shared presentation package Replace BillingScreen with Overview/Usage navigator, move presentation into @beakerstack/billing, and remove mobile upgrade/checkout CTAs for App Store policy. * fix(mobile): address PR review feedback on billing screens Harden plan feature reads, hook error handling, a11y, URL redaction, redirect allowlist normalization, and add billing screen tests. * fix(mobile): use literal apostrophe on billing overview plan title React Native does not decode HTML entities; You're matches the billing screen test and PlanFeatureList.native. * fix(web): scroll to top on route navigation (#222) * fix(web): scroll to top on route navigation Closes #216 Adds ScrollToTop component that calls window.scrollTo(0, 0) whenever the pathname changes. Hash-only changes (in-page anchors) are deliberately excluded. Component is placed in App before Routes so it fires for every client-side navigation across the whole app. * fix review: useLayoutEffect, focus reset, spy cleanup - useLayoutEffect so scroll runs before paint (no flash at old position) - Render a zero-size fixed span as focus target; focus it on pathname change with preventScroll so the two calls don't fight each other - afterEach vi.restoreAllMocks() so scrollTo spy doesn't leak between tests * fix(web): standardize public header — delegate PolicyPublicHeader to Nav (issue #215) (#223) * fix(web): standardize public header — delegate PolicyPublicHeader to Nav Rewrites PolicyPublicHeader as a thin wrapper around the landing Nav component so policy pages get sticky-on-scroll behavior, the mobile menu, and Features/Pricing/FAQ nav links that match the home page. Hash anchors are prefixed with '/' (e.g. /#features) so they navigate home-then-scroll when followed from /terms or /privacy. Closes #215 * fix(test): import act from @testing-library/react not vitest * fix(PolicyPublicHeader): compute nav config inside component for PR preview path support Hash anchors were built at module load time, hardcoding "/" as the base path. On PR preview deployments the base path is "/pr-N/", so clicking Features/Pricing/FAQ from a policy page would land at the domain root instead of the preview slug. Moving publicNavConfig construction inside the component function means getPrPreviewAssetBasePath() is called at render time, when window.location.pathname reflects the actual deployment path. * fix(web): landing and app UI polish — images, layout, nav, and branding (#224) * feat(web): update single-codebase feature hero image Replace mobile-hero.avif with a web+mobile composite on a white background, using contain scaling so the full device frame is preserved. * fix(web): standardize layout width at 1024px with shared ContentContainer Align marketing and app chrome on max-w-content (1024px) via a shared container and Tailwind tokens, with prose variant (800px) for policy/FAQ. Ensure JIT generates width utilities by scanning shared layout config. * fix(web): scroll to hash sections instead of top on cross-page nav ScrollToTop was resetting scroll on every pathname change, which broke Features/Pricing/FAQ links from policy pages. Honor location.hash and retry until lazy landing sections mount. * feat(web): add main hero image and update SEO feature art Use a dedicated hero.avif for the headline carousel and replace seo.avif with Lighthouse scores, keeping mobile-hero.avif for the single-codebase feature row. * fix(branding): standardize user-facing copy on Beaker Stack Wire marketing landing and SEO meta to BRANDING.displayName so app chrome, policies, and landing page stay consistent; keep BeakerStack for technical IDs. * fix(web): keep app header sticky on scroll Match public Nav behavior so dashboard, billing, and auth pages pin the header with scroll-gated shadow. * fix(web): align dashboard demo banner tests with Beaker Stack branding * fix(web): address PR review feedback on scroll, seeds, and layout Harden hash scrolling with safe decode, cancellable rAF retries, and dev warnings; upsert billing product display names in seeds; remove double padding on ProfilePage; clarify BRANDING docs and simplify landing test. * fix(web): use import.meta.env.DEV for hash scroll warning * docs: remove stale task lists and billing review artifacts Delete historical TASKS.md, one-time billing reviews, and deprecated billing-demo stub. Refresh billing specs and docs index for shipped /billing. * Revert "docs: remove stale task lists and billing review artifacts" This reverts commit fb1e5eb. * docs: remove stale task lists and billing review artifacts (#225) Delete historical TASKS.md, one-time billing reviews, and deprecated billing-demo stub. Refresh billing specs and docs index for shipped /billing. --------- Co-authored-by: Sarah Mitchell 🤖 <sarah@artificerinnovations.com> Co-authored-by: Mei Zhang 🤖 <mei@artificerinnovations.com> Co-authored-by: Diego Morales 🤖 <diego@artificerinnovations.com> Co-authored-by: Rahul Iyer 🤖 <rahul@artificerinnovations.com>
Summary
SecurityHeadersPolicy(67f7725c-...) with a customAWS::CloudFront::ResponseHeadersPolicyacross all three CloudFront distributions (Prod, Staging, Deploy/Preview)Content-Security-Policy-Report-Onlyheader for Phase 1 violation monitoringid="csp-inline-theme"to the anti-FOUC inline script inindex.htmlso the CI hash check can locate it reliablyscripts/check-csp-hash.mjs+.github/workflows/check-csp-hash.yml— CI asserts the inline script SHA-256 matches the hash in the CloudFormation policy; prints the corrected hash and instructions if it failsCSP string
The
sha256-...value covers the exact bytes of the anti-FOUC theme script inindex.html— nounsafe-inlineneeded inscript-src.Test plan
Content-Security-Policy-Report-Onlyheader is present in CloudFront responsesnode scripts/check-csp-hash.mjsCloses #144 (Phase 1). Phase 2 enforcement tracked in #153.
🤖 Generated with Claude Code