diff --git a/.changeset/add-plugin-content-schema-extension.md b/.changeset/add-plugin-content-schema-extension.md new file mode 100644 index 00000000..36573fd7 --- /dev/null +++ b/.changeset/add-plugin-content-schema-extension.md @@ -0,0 +1,7 @@ +--- +"@stackwright/types": minor +--- + +Add `contentItemSchemas` and `knownContentTypeKeys` to `PrebuildPlugin` interface. +Add `buildExtendedPageContentSchema()` function for merging OSS and plugin content schemas. +Add `ValidatePageContentOptions` to `validatePageContent()` for plugin-aware validation. diff --git a/.changeset/add-pro-content-normalization.md b/.changeset/add-pro-content-normalization.md new file mode 100644 index 00000000..8e63d51e --- /dev/null +++ b/.changeset/add-pro-content-normalization.md @@ -0,0 +1,6 @@ +--- +"@stackwright/build-scripts": minor +--- + +Add content format normalization (mapping-key YAML format → type-field format) to prebuild pipeline. +Plugin `contentItemSchemas` and `knownContentTypeKeys` are now applied during page validation. diff --git a/.changeset/dependabot-batch-updates.md b/.changeset/dependabot-batch-updates.md new file mode 100644 index 00000000..d5b22d58 --- /dev/null +++ b/.changeset/dependabot-batch-updates.md @@ -0,0 +1,15 @@ +--- +"@stackwright/core": patch +"@stackwright/icons": patch +"@stackwright/maplibre": patch +"@stackwright/nextjs": patch +"@stackwright/ui-shadcn": patch +--- + +chore: consolidate dependabot dependency updates + +- `lucide-react`: `^0.525.0` → `^1.8.0` (icons, ui-shadcn) — includes icon rename fixes for v1 API (`CheckCircle` → `CircleCheck`, `Code2`/`Layout` backward-compat aliases) +- `@swc/core`: `^1.15.18` → `^1.15.26` (core, nextjs) +- `jsdom`: `^28.1.0` → `^29.0.2` (maplibre) +- `react-dom`: `19.2.4` → `19.2.5` (pnpm.overrides) +- `prettier`: `^3.8.1` → `^3.8.3` (devDependencies) diff --git a/.changeset/feat-188-page-add-content-flag.md b/.changeset/feat-188-page-add-content-flag.md new file mode 100644 index 00000000..eab52a5e --- /dev/null +++ b/.changeset/feat-188-page-add-content-flag.md @@ -0,0 +1,7 @@ +--- +"@stackwright/cli": patch +--- + +feat(cli): add --content flag to `page add` for inline YAML (#188) + +Agents can now create a page with full content in a single command instead of a two-step add + write sequence. Content is validated before writing; invalid YAML is rejected with field-level errors. diff --git a/.changeset/feat-243-security-headers.md b/.changeset/feat-243-security-headers.md new file mode 100644 index 00000000..76a47d9a --- /dev/null +++ b/.changeset/feat-243-security-headers.md @@ -0,0 +1,5 @@ +--- +"@stackwright/nextjs": minor +--- + +Add security headers (CSP, HSTS, COOP/CORP/COEP) to Next.js integration with customizable configuration diff --git a/.changeset/fix-352-install-flag-actually-installs.md b/.changeset/fix-352-install-flag-actually-installs.md new file mode 100644 index 00000000..2cf34eda --- /dev/null +++ b/.changeset/fix-352-install-flag-actually-installs.md @@ -0,0 +1,5 @@ +--- +"@stackwright/cli": patch +--- + +fix(cli): --install flag now runs pnpm install before postInstall hooks diff --git a/.changeset/fix-plugin-config-schema-242.md b/.changeset/fix-plugin-config-schema-242.md new file mode 100644 index 00000000..88db2c67 --- /dev/null +++ b/.changeset/fix-plugin-config-schema-242.md @@ -0,0 +1,6 @@ +--- +"@stackwright/types": patch +"@stackwright/build-scripts": patch +--- + +Add configSchema field to PrebuildPlugin for plugin config validation diff --git a/.changeset/fix-plugin-this-binding.md b/.changeset/fix-plugin-this-binding.md new file mode 100644 index 00000000..dbb13b74 --- /dev/null +++ b/.changeset/fix-plugin-this-binding.md @@ -0,0 +1,14 @@ +--- +"@stackwright/build-scripts": patch +--- + +fix(executePluginHook): preserve `this` binding when calling plugin lifecycle hooks + +`executePluginHook` was extracting hook methods as unbound references +(`const hookFn = plugin[hook]`) and calling them as plain functions +(`hookFn(context)`). In strict-mode ES classes, this strips `this`, +causing any plugin that calls a private/instance method from `beforeBuild` +or `afterBuild` to throw `Cannot read properties of undefined`. + +Fix: use `hookFn.call(plugin, context)` so the plugin instance is always +the receiver. diff --git a/.changeset/fix-preinstall-double-run.md b/.changeset/fix-preinstall-double-run.md new file mode 100644 index 00000000..df264ead --- /dev/null +++ b/.changeset/fix-preinstall-double-run.md @@ -0,0 +1,18 @@ +--- +"@stackwright/cli": patch +--- + +fix(cli): remove duplicate preInstall hook call from processTemplate + +`processTemplate()` was calling `runScaffoldHooks('preInstall', ...)` internally, +then `scaffold.ts` called it again after `processTemplate` returned — running every +preInstall handler twice. Worse, the second call passed the original empty `{}` object +(not the built package.json), so hooks registered via `scaffold.ts` could never affect +the written file. + +Fix: lifecycle orchestration now lives entirely in `scaffold.ts`. `buildPackageJson` is +exported so `scaffold.ts` can build the default package.json before running preInstall +hooks, then passes the already-hooks-modified object into `processTemplate` for writing. +`processTemplate` no longer calls hooks. + +Fixes #351. diff --git a/.changeset/grumpy-paws-create.md b/.changeset/grumpy-paws-create.md new file mode 100644 index 00000000..3394347b --- /dev/null +++ b/.changeset/grumpy-paws-create.md @@ -0,0 +1,5 @@ +--- +"@stackwright/core": patch +--- + +fix(core): prevent duplicate TopAppBar rendering that caused a double dark-mode toggle icon diff --git a/.changeset/happy-books-accept.md b/.changeset/happy-books-accept.md deleted file mode 100644 index 8f86a9d9..00000000 --- a/.changeset/happy-books-accept.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"stackwright": patch ---- - -fix(ci): update GitHub Actions to latest versions - -- actions/checkout@v5 (was v4) -- actions/setup-node@v5 (was v4) -- pnpm/action-setup@v4 (was v3) -- Node 22 (was 20 in deploy-docs and prerelease) -- Add deploy-docs.yml to its own path triggers - -Updated composite action and all workflows to use latest action versions. diff --git a/.changeset/pre.json b/.changeset/pre.json index 38bb5c43..361d1a8c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -18,21 +18,31 @@ "@stackwright/scaffold-core": "0.1.0-alpha.1", "@stackwright/themes": "0.5.1-alpha.0", "@stackwright/types": "1.1.0-alpha.6", - "@stackwright/ui-shadcn": "0.1.0" + "@stackwright/ui-shadcn": "0.1.0", + "@stackwright/hooks-registry": "0.1.0-alpha.0" }, "changesets": [ "add-code2-layout-icons", + "add-image-dimension-validation", "bright-otters-glow", "built-in-search-feature", "compose-site-atomic", "declarative-entry-pages", + "dependabot-batch-updates", "docs-architecture-principles", + "feat-243-security-headers", + "feat-env-var-secrets-245", + "fix-352-install-flag-actually-installs", "fix-cli-scaffold-smoke-test", "fix-dark-mode-bugs", "fix-dark-mode", "fix-icons-architecture-codeblock", + "fix-issue-339-icon-theme-tokens", "fix-maplibre-lockfile", + "fix-plugin-config-schema-242", + "fix-preinstall-double-run", "fix-unpin-otter-models", + "grumpy-paws-create", "integrations-config", "launch-stackwright-package", "map-adapter-phases-1-2", diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 9731a4bd..e85b421a 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -55,7 +55,7 @@ jobs: - name: Parse test results and comment on PR if: github.event_name == 'pull_request' && always() - uses: actions/github-script@v7 + uses: actions/github-script@v9 continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e0ec3002..026dc378 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -47,7 +47,7 @@ jobs: - name: Comment PR with coverage if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v9 continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e5b55f0b..6b07d7e5 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -43,7 +43,7 @@ jobs: fetch-depth: 0 - name: Install pnpm - run: npm install -g pnpm@10.30.3 + run: npm install -g pnpm@10.33.0 - name: Setup Node.js uses: actions/setup-node@v5 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 3590338e..c7a571ea 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -2,7 +2,7 @@ name: Performance Benchmarks on: pull_request: - branches: [main, develop] + branches: [main, dev] workflow_dispatch: inputs: run-all: @@ -27,6 +27,9 @@ jobs: build: true relink-bins: true + - name: Install Playwright browsers + run: pnpm --filter @stackwright/e2e exec playwright install --with-deps chromium + - name: ⚡ Run build time benchmarks id: build-time env: @@ -91,7 +94,7 @@ jobs: - name: 💬 Comment PR with results if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v9 continue-on-error: true with: script: | diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index c1e582d6..be351fa1 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -27,7 +27,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: "10.30.3" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d5234e8..ce0bb9f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: token: ${{ steps.app-token.outputs.token }} fetch-depth: 0 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: "10.30.3" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index a1a35bba..4379cf6d 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -29,9 +29,9 @@ jobs: fetch-depth: 0 # Full history for gitleaks - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: '1.21' + go-version: '1.24' - name: Install Gitleaks run: go install github.com/gitleaks/gitleaks/v9@latest @@ -47,9 +47,7 @@ jobs: uses: actions/checkout@v4 - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 + uses: pnpm/action-setup@v6 - name: Setup Node.js uses: actions/setup-node@v4 @@ -71,9 +69,7 @@ jobs: uses: actions/checkout@v4 - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 + uses: pnpm/action-setup@v6 - name: Setup Node.js uses: actions/setup-node@v4 @@ -95,7 +91,7 @@ jobs: - name: Upload Semgrep SARIF if: always() && -f semgrep.sarif - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: semgrep.sarif category: semgrep diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 489627d2..e2ca5a5a 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -63,7 +63,7 @@ jobs: - name: Comment PR with results if: always() && github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/CLAUDE.md b/CLAUDE.md index edfdd03e..df12d0f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -325,6 +325,17 @@ Core components (`packages/core/src/components/`) use **inline `style={{}}` prop - For flex layouts that must stack on mobile, use `flexWrap: 'wrap'` with a `minWidth` on children to control the wrap breakpoint. Use `minWidth: 'min(Xpx, 100%)'` to prevent overflow on very narrow viewports. - For text that may overflow on narrow viewports (emails, URLs, long strings), add `wordBreak: 'break-word'` or `wordBreak: 'break-all'` as appropriate. +### Security Headers + +Stackwright projects should implement security headers for defense in depth. See [docs/CSP-BEST-PRACTICES.md](./docs/CSP-BEST-PRACTICES.md) for: +- Complete `next.config.js` CSP configuration +- Next.js App Router (middleware.ts) patterns +- Google Fonts-specific directives +- Permissions-Policy recommendations +- Common gotchas and testing strategies + +Quick reference snippet: [docs/snippets/CSP-QUICK-REF.js](./docs/snippets/CSP-QUICK-REF.js) + ### Image Co-location Pipeline Images can be co-located with their page YAML files in `pages/`. Using a relative path starting with `./` in YAML (e.g., `src: ./hero-image.png`) triggers automatic processing during the prebuild step: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43b5ce39..19963d13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,7 +146,7 @@ Good commit points: - After adding a new module or file that compiles/passes lint - After wiring up a new feature end-to-end (even before tests) - After adding or updating tests for the feature -- After updating docs, ROADMAP.md, or changesets +- After updating docs or changesets - Before and after a refactor that touches many files Commit messages should be concise and use conventional commit prefixes (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`). Include the issue number when relevant (e.g., `feat(build-scripts): add --watch mode (#122)`). @@ -266,7 +266,7 @@ The AGENTS.md tables are auto-generated from the live Zod schemas. Do NOT edit t ## Priority Labels & Product Board -Work is tracked via GitHub Issues with priority labels. `ROADMAP.md` is a narrative document describing architectural direction — not a task tracker. +Work is tracked via GitHub Issues with priority labels. GitHub Issues are the single source of truth for planned work — run `pnpm stackwright -- board` to see the prioritized board. | Label | Meaning | |-------|--------| @@ -286,7 +286,7 @@ pnpm stackwright -- board --json Agents can call `stackwright_get_board` via MCP for the same data. -The architect sets priority tiers. Contributors and agents should pick work from `priority:now` first, then `priority:next`. When a PR closes an issue, GitHub handles it automatically — no manual ROADMAP.md updates needed. +The architect sets priority tiers. Contributors and agents should pick work from `priority:now` first, then `priority:next`. When a PR closes an issue, GitHub handles it automatically. ## Package Structure diff --git a/PHILOSOPHY.md b/PHILOSOPHY.md index 5e5b8e3f..2c9a9b00 100644 --- a/PHILOSOPHY.md +++ b/PHILOSOPHY.md @@ -4,7 +4,7 @@ This document captures the product intent and architectural principles behind St Stackwright's one-sentence thesis: **Visual rendering + constrained DSL + AI iteration = non-technical people building enterprise apps that are safe by construction.** -[CONTRIBUTING.md](./CONTRIBUTING.md) tells you how to work in this repo. [ROADMAP.md](./ROADMAP.md) tells you what to build next. This document tells you what Stackwright is and why it is built the way it is. +[CONTRIBUTING.md](./CONTRIBUTING.md) tells you how to work in this repo. For the live list of what's being worked on, run `pnpm stackwright -- board` or see the [GitHub Issues](https://github.com/Per-Aspera-LLC/stackwright/issues). This document tells you what Stackwright is and why it is built the way it is. --- @@ -269,4 +269,3 @@ For contributors and agents making implementation decisions: 5. **Agent-facing docs are part of the build.** The content type reference tables in AGENTS.md must be kept in sync with the TypeScript types. This is as important as keeping the JSON schemas in sync. Stale agent docs produce exactly the same class of bugs as stale type definitions. 6. **Constrain first, extend later — in the free tier.** When in doubt about whether to add a new content type or field to `@stackwright/core`, wait. The cost of adding something is low; the cost of maintaining it, keeping it in the schema reference, making it agent-writable, and eventually removing it is high. The right answer to "I need something the core schema doesn't support" is either a developer-written React component or a pro component package — not a core schema extension. This principle does not apply to pro packages, which exist specifically to serve specialized use cases. -t support" is either a developer-written React component or a pro component package — not a core schema extension. This principle does not apply to pro packages, which exist specifically to serve specialized use cases. diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 43c03c27..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,95 +0,0 @@ -# Stackwright Roadmap - -This document describes the *direction* of the project — the architectural rationale, the product trajectory, and the vision that shapes prioritization. It is narrative, not a checklist. - -**For the live, prioritized list of what's being worked on:** -```bash -# Terminal (humans) -pnpm stackwright -- board - -# MCP tool (agents) -stackwright_get_board -``` - -These query open GitHub Issues sorted by [priority labels](#priority-labels). Issues are the single source of truth for planned work. - ---- - -## Grammar Hardening - -The type system that defines what YAML can express — the Stackwright grammar — is the product's core moat. The grammar must be rigorous, introspectable, and extensible. - -**Completed foundation:** Zod schemas are the single source of truth for the grammar. TypeScript types are inferred via `z.infer<>`. JSON schemas are generated for IDE YAML validation. Runtime validation runs in the prebuild pipeline. Content types are extensible via `registerContentType()`. MCP tools introspect the schema at runtime. - -**Next architectural step: explicit `type` field (#131).** The content renderer currently uses `Object.entries(item)[0]` to discriminate content types — a pattern that relies on JS object insertion order, prevents TypeScript discriminated unions, and produces poor error messages. Migrating to an explicit `type` field on every content item is a breaking change that enables proper discriminated union narrowing and clearer validation errors. This is sequenced after the grammar foundation is solid because it touches every YAML file, test, and content type. - ---- - -## Visual Rendering & AI Design Loop - -**Shipped.** The visual rendering infrastructure is now in place — AI agents and the CLI can screenshot pages, preview raw YAML, and capture before/after diffs. - -**What's live:** -- MCP tools: `stackwright_render_page`, `stackwright_render_diff`, `stackwright_render_yaml`, `stackwright_check_dev_server` -- CLI: `stackwright preview` command -- E2E tests: full render pipeline verified against the example app -- **Brand Otter** — part of the Otter Raft, discovers brand through conversation and produces BRAND_BRIEF.md (see `./packages/otters/README.md`) - -**Next step: branding expert iteration loop.** With Brand Otter shipped, the next evolution is enabling AI agents to visually iterate on themes — generate variations, render each, evaluate against brand criteria, and converge on the right feel. This builds on the visual rendering infrastructure now in place. - -This iteration loop is the proof point for the platform's thesis: that non-technical people can build professional, brand-appropriate applications through conversation — with the constrained DSL guaranteeing safety and the visual feedback loop guaranteeing quality. - ---- - -## Framework Direction - -Stackwright ships 18 content types (carousel, main, tabbed_content, media, video, timeline, icon_grid, code_block, feature_list, testimonial_grid, faq, pricing_table, alert, contact_form_stub, grid, collection_list, text_block, map). Dark mode, SEO metadata, cookie persistence, and responsive design are first-class. - -**Next framework priorities** are tracked as GitHub Issues. Themes from PHILOSOPHY.md that shape prioritization: - -- **Constrain first, extend later.** New content types should represent genuinely distinct layout patterns, not one-off customizations. The bar for adding to `@stackwright/core` is deliberately high. -- **The escape hatch is a feature.** Every architectural decision must preserve the ability for a React developer to open the project and extend it without knowing Stackwright exists. -- **AI writes the YAML; the framework enforces correctness.** Schema reliability matters more than schema expressiveness. - ---- - -## Monetization Path - -See `.claude/stackwright-pro-vision.md` for the full product vision. Summary of the trajectory: - -**Near-term: CMS collection providers.** Third-party CMS SDKs (Contentful, Sanity, Shopify, Airtable, Notion) break frequently. Maintaining these integrations as `@stackwright-pro/collections-*` packages is a real, ongoing service — not a one-time implementation. Each implements the `CollectionProvider` interface; switching backends is a one-line change. - -**Medium-term: OpenAPI integration.** `@stackwright-pro/openapi` takes an OpenAPI spec and emits a typed `CollectionProvider`, Zod schemas, TypeScript types, and a client module. Turns "here is an API spec" into "here is a working, validated UI" in hours. - -**Long-term: safe enterprise application platform.** YAML-defined backend components — data tables, forms, approval flows, API integrations — all constrained by Zod schemas, all verifiably safe by construction. Subject matter experts define workflows; the platform enforces safety. The same compositional approach — declarative, schema-constrained, AI-writable, visually verifiable — extended from marketing sites to enterprise applications. The one-sentence pitch: visual rendering + constrained DSL + AI iteration = non-technical people building enterprise apps that are safe by construction. - -**Adjacent opportunities:** AI-powered project scaffolding, visual editor backed by the MCP server, data-interactive component library (charts, tables, forms). - ---- - -## Infrastructure Direction - -The MCP server is the primary non-developer interface (see PHILOSOPHY.md: "The GUI Is AI"). It currently provides 20 tools spanning content authoring, site configuration, visual rendering, git workflow, and project management. - -**Recent milestones:** -- Visual rendering tools shipped — AI agents can now see their output -- `stackwright preview` CLI command — screenshot pages from the terminal -- E2E render pipeline tests — visual tooling verified end-to-end -- Whole-site composition (`stackwright_compose_site`) with cross-page validation - -**Next infra priorities:** Branding expert agent, E2E screenshot comparison on merge (#141), AI-driven visual QA. All build on the visual rendering infrastructure now in place. - ---- - -## Priority Labels - -Issues use four priority labels. The architect sets tiers; agents and contributors respect them. - -| Label | Meaning | -|-------|---------| -| `priority:now` 🔴 | Actively in progress or next up | -| `priority:next` 🟡 | Committed — starting soon | -| `priority:later` 🟢 | Planned but not yet committed | -| `priority:vision` 🟣 | Aspirational — shapes direction, no timeline | - -Run `pnpm stackwright -- board` to see the current board, or call `stackwright_get_board` via MCP. diff --git a/docs/CSP-BEST-PRACTICES.md b/docs/CSP-BEST-PRACTICES.md new file mode 100644 index 00000000..4d336481 --- /dev/null +++ b/docs/CSP-BEST-PRACTICES.md @@ -0,0 +1,555 @@ +# Content Security Policy (CSP) Best Practices for Next.js + +*This guide provides practical, implementable patterns for securing Next.js applications with CSP headers, security headers, and Permissions-Policy directives.* + +--- + +## Table of Contents + +1. [Quick Start: Complete next.config.js Example](#quick-start-complete-nextconfigjs-example) +2. [Recommended CSP Directives](#recommended-csp-directives) +3. [Next.js 14/15 App Router Patterns](#nextjs-1415-app-router-patterns) +4. [Google Fonts Configuration](#google-fonts-configuration) +5. [Permissions-Policy Directives](#permissions-policy-directives) +6. [Common Gotchas & Mistakes](#common-gotchas--mistakes) +7. [Testing Your CSP](#testing-your-csp) +8. [Report-URI & CSP Violation Monitoring](#report-uri--csp-violation-monitoring) + +--- + +## Quick Start: Complete next.config.js Example + +```javascript +// next.config.js +const securityHeaders = [ + { + // Content Security Policy + key: 'Content-Security-Policy', + value: ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + font-src 'self' https://fonts.gstatic.com; + img-src 'self' data: https://images.unsplash.com https://*.unsplash.com; + connect-src 'self' https://api.example.com wss://*.example.com; + frame-src 'none'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; + upgrade-insecure-requests; + `.replace(/\s{2,}/g, ' ').trim(), + }, + { + // X-Content-Type-Options prevents MIME type sniffing + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + // X-Frame-Options prevents clickjacking + key: 'X-Frame-Options', + value: 'DENY', + }, + { + // X-XSS-Protection (legacy browsers) + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + // Referrer-Policy controls information in the Referer header + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + // Permissions-Policy restricts browser features + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', + }, + { + // Strict-Transport-Security enforces HTTPS + key: 'Strict-Transport-Security', + value: 'max-age=31536000; includeSubDomains; preload', + }, +]; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + async headers() { + return [ + { + source: '/(.*)', + headers: securityHeaders, + }, + ]; + }, +}; + +module.exports = nextConfig; +``` + +--- + +## Recommended CSP Directives + +### Core Directives (Required) + +| Directive | Value | Purpose | +|-----------|-------|---------| +| `default-src` | `'self'` | Fallback for all resource types | +| `script-src` | `'self'` | Restrict JavaScript sources | +| `style-src` | `'self' 'unsafe-inline'` | Stylesheets (unsafe-inline often needed for Next.js) | +| `img-src` | `'self' data: https:` | Images (data: for inline images, https: for external) | +| `font-src` | `'self' https://fonts.gstatic.com` | Web fonts | +| `connect-src` | `'self'` | AJAX, WebSocket, fetch sources | +| `frame-src` | `'none'` | Prevent embedding in iframes | +| `object-src` | `'none'` | Disable Flash and plugins | +| `base-uri` | `'self'` | Restrict `` element | +| `form-action` | `'self'` | Restrict form submission targets | +| `frame-ancestors` | `'none'` | Prevent clickjacking | + +### Directive Values Explained + +```javascript +// Common source values +'self' // Same origin only +'none' // Block completely +'unsafe-inline' // Allow inline scripts/styles (⚠️ weakens CSP) +'unsafe-eval' // Allow eval() (⚠️ security risk) +'nonce-abc123' // Allow specific inline script with nonce +data: // Data URLs (base64 images, etc.) +https: // All HTTPS URLs (any domain) +'domain.com' // Specific domain +'*.domain.com' // Any subdomain +``` + +### Script-Src Options for Next.js + +Next.js has specific script loading requirements: + +```javascript +// Conservative (requires work for Next.js) +'script-src': "'self'" + +// Recommended for most Next.js apps +'script-src': "'self' 'unsafe-inline' 'unsafe-eval'" + +// If using inline scripts with nonces (most secure) +// Requires nonce generation in _document.tsx +'script-src': "'self' 'nonce-{NONCE_VALUE}'" +``` + +--- + +## Next.js 14/15 App Router Patterns + +### App Router (app/) + +For Next.js 14+ with App Router, use middleware for headers: + +```typescript +// middleware.ts +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const securityHeaders = { + 'Content-Security-Policy': ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + font-src 'self' https://fonts.gstatic.com; + img-src 'self' data: https: blob:; + connect-src 'self' https://api.example.com; + frame-ancestors 'none'; + base-uri 'self'; + form-action 'self'; + `.replace(/\s{2,}/g, ' ').trim(), + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', +}; + +export function middleware(request: NextRequest) { + const response = NextResponse.next(); + + Object.entries(securityHeaders).forEach(([key, value]) => { + response.headers.set(key, value); + }); + + return response; +} + +export const config = { + matcher: [ + // Match all paths except static files and images + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; +``` + +### Pages Router (_app.tsx approach) + +```javascript +// next.config.js +module.exports = { + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';", + }, + ], + }, + ]; + }, +}; +``` + +--- + +## Google Fonts Configuration + +### Using Next.js Font Module (Recommended) + +```javascript +// app/layout.tsx +import { Inter } from 'next/font/google'; + +const inter = Inter({ + subsets: ['latin'], + display: 'swap', + variable: '--font-inter', +}); + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} +``` + +CSP for Next.js fonts (automatically handled): +```javascript +'font-src': "'self' https://fonts.gstatic.com" +``` + +### Using Google Fonts Link Tag + +If using `` tags for Google Fonts: + +```javascript +// next.config.js +const ContentSecurityPolicy = ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + font-src 'self' https://fonts.gstatic.com; + img-src 'self' data: https:; + connect-src 'self'; + frame-ancestors 'none'; + base-uri 'self'; +`.replace(/\s{2,}/g, ' ').trim(); + +// Add to headers... +``` + +### Preconnect for Performance + +```html + + + +``` + +--- + +## Permissions-Policy Directives + +The `Permissions-Policy` header controls browser APIs and features. + +### Common Directives + +```javascript +// Disable all features by default +'Permissions-Policy': 'fullscreen=(self), microphone=(), camera=(), geolocation=(), payment=()' + +// More granular control +'Permissions-Policy': ` + camera=(self "https://example.com"), + microphone=(self "https://example.com"), + geolocation=(self), + payment=(), + usb=(), + interest-cohort=() +`.replace(/\s{2,}/g, ' ').trim() +``` + +### Directive Reference + +| Directive | Recommended | Notes | +|-----------|-------------|-------| +| `geolocation` | `()` | Disable location tracking | +| `microphone` | `()` | Disable microphone access | +| `camera` | `()` | Disable camera access | +| `payment` | `()` | Disable payment API unless needed | +| `usb` | `()` | Disable USB access | +| `interest-cohort` | `()` | Disable FLoC tracking | +| `accelerometer` | `()` | Disable motion sensors | +| `autoplay` | `()` | Control media autoplay | +| `fullscreen` | `(self)` | Allow only same-origin | +| `web-share` | `()` | Disable Web Share API | + +--- + +## Common Gotchas & Mistakes + +### 1. **Whitespace in CSP Values** + +```javascript +// ❌ WRONG - extra whitespace breaks CSP parsing +value: ` + default-src 'self'; + script-src 'self'; +` + +// ✅ CORRECT - normalize whitespace +value: ` + default-src 'self'; + script-src 'self'; +`.replace(/\s{2,}/g, ' ').trim() +``` + +### 2. **Forgetting upgrade-insecure-requests** + +```javascript +// ✅ Include this to auto-upgrade HTTP to HTTPS +'upgrade-insecure-requests' +``` + +### 3. **Blocking Google Fonts** + +If fonts don't load: +```javascript +'font-src': "'self' https://fonts.gstatic.com" +'style-src': "'self' 'unsafe-inline' https://fonts.googleapis.com" +``` + +### 4. **Blocking Images in Next.js Image Component** + +```javascript +// Next.js Image component needs these +'img-src': "'self' data: blob: https://images.unsplash.com https://*.unsplash.com https://*.maptiler.com" +``` + +### 5. **Blocking WebSocket Connections** + +```javascript +// If using real-time features +'connect-src': "'self' wss://realtime.example.com https://api.example.com" +``` + +### 6. **Report-Only Mode for Testing** + +```javascript +// Use Content-Security-Policy-Report-Only during testing +key: 'Content-Security-Policy-Report-Only', +value: ` + default-src 'self'; + report-uri /api/csp-violations; + report-to csp-group; +`.trim() +``` + +### 7. **nonce-* Not Working with React 18** + +React 18's streaming SSR doesn't fully support nonces yet: +- Stick with `'unsafe-inline'` for now in most cases +- Or use `report-only` mode for monitoring while developing + +--- + +## Testing Your CSP + +### Using Report-Only Mode + +```javascript +// next.config.js +async headers() { + return [ + { + source: '/(.*)', + headers: [ + // Report-only mode - doesn't block, just reports violations + { + key: 'Content-Security-Policy-Report-Only', + value: ` + default-src 'self'; + script-src 'self' 'unsafe-inline'; + report-uri /api/csp-report; + report-to csp-endpoint; + `.replace(/\s{2,}/g, ' ').trim(), + }, + ], + }, + ]; +} +``` + +### CSP Evaluator + +Use Google's [CSP Evaluator](https://csp-evaluator.withgoogle.com/) to check your CSP for weaknesses. + +### Browser DevTools + +1. Open DevTools → Console +2. CSP violations appear as red error messages +3. Network tab shows blocked resources + +### Automated Testing + +```javascript +// test/security-headers.test.ts +describe('Security Headers', () => { + it('should have CSP header', async () => { + const response = await fetch('https://yoursite.com'); + const csp = response.headers.get('Content-Security-Policy'); + expect(csp).toBeTruthy(); + expect(csp).toContain("default-src 'self'"); + }); + + it('should have X-Frame-Options set to DENY', async () => { + const response = await fetch('https://yoursite.com'); + const xfo = response.headers.get('X-Frame-Options'); + expect(xfo).toBe('DENY'); + }); +}); +``` + +--- + +## Report-URI & CSP Violation Monitoring + +### Setting Up Violation Reports + +```javascript +// next.config.js +const ContentSecurityPolicy = ` + default-src 'self'; + script-src 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: https:; + report-uri https://your-csp-reporter.example.com/api/csp-report; + report-to csp-group; +`.trim(); +``` + +### Report-To Configuration + +```javascript +// In headers +{ + key: 'Report-To', + value: JSON.stringify({ + group: 'csp-group', + max_age: 10886400, + endpoints: [ + { url: 'https://your-reporter.example.com/csp-reports' } + ], + }), +} +``` + +### Free CSP Monitoring Services + +- **CSP Auditor** (https://csper.io) +- **Report URI** (https://report-uri.com) +- **Safetix** (https://safetix.io) + +--- + +## Stackwright Integration + +Stackwright projects can add CSP headers via `createStackwrightNextConfig()`: + +```javascript +// next.config.js +const { createStackwrightNextConfig } = require('@stackwright/nextjs'); + +const securityHeaders = [ + { + key: 'Content-Security-Policy', + value: ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + font-src 'self' https://fonts.gstatic.com; + img-src 'self' data: https: blob:; + connect-src 'self'; + frame-ancestors 'none'; + base-uri 'self'; + form-action 'self'; + object-src 'none'; + upgrade-insecure-requests; + `.replace(/\s{2,}/g, ' ').trim(), + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', + }, +]; + +const nextConfig = createStackwrightNextConfig({ + async headers() { + return [ + { + source: '/(.*)', + headers: securityHeaders, + }, + ]; + }, +}); + +module.exports = nextConfig; +``` + +--- + +## Summary Checklist + +- [ ] Add `Content-Security-Policy` header +- [ ] Include `X-Content-Type-Options: nosniff` +- [ ] Add `X-Frame-Options: DENY` +- [ ] Set `Referrer-Policy` +- [ ] Configure `Permissions-Policy` to disable unused features +- [ ] Enable `Strict-Transport-Security` +- [ ] Add `upgrade-insecure-requests` directive +- [ ] Whitelist Google Fonts domains +- [ ] Test with `report-only` mode first +- [ ] Monitor violations before enforcing + +--- + +## References + +- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) +- [MDN CSP Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) +- [Google CSP Evaluator](https://csp-evaluator.withgoogle.com/) +- [Next.js Security Headers Guide](https://nextjs.org/docs/app/guides/security-headers) +- [Mozilla Observatory](https://observatory.mozilla.org/) diff --git a/docs/PLUGIN_SECURITY.md b/docs/PLUGIN_SECURITY.md index 42a54a91..41107cfa 100644 --- a/docs/PLUGIN_SECURITY.md +++ b/docs/PLUGIN_SECURITY.md @@ -1,6 +1,6 @@ # Plugin Security Guide -*This guide establishes security requirements and best practices for Stackwright plugin developers. For framework security architecture, see [security-model-for-docs.md](./security-model-for-docs.md).* +*This guide establishes security requirements and best practices for Stackwright plugin developers. For framework security architecture, see [SECURITY-MODEL.md](./SECURITY-MODEL.md).* --- @@ -336,7 +336,7 @@ Contact security@per-aspera.dev with: - [CONTRIBUTING.md](../CONTRIBUTING.md) — General contribution guidelines - [AGENTS.md](../AGENTS.md) — AI agent guidance -- [security-model-for-docs.md](./security-model-for-docs.md) — Framework security architecture +- [SECURITY-MODEL.md](./SECURITY-MODEL.md) — Framework security architecture --- diff --git a/docs/security-model-for-docs.md b/docs/SECURITY-MODEL.md similarity index 80% rename from docs/security-model-for-docs.md rename to docs/SECURITY-MODEL.md index 33274587..f151939b 100644 --- a/docs/security-model-for-docs.md +++ b/docs/SECURITY-MODEL.md @@ -6,6 +6,28 @@ ## Two-Layer Security Model +### Layer 0: HTTP Security Headers + +Stackwright applications include comprehensive HTTP security headers by default: + +1. **Content-Security-Policy (CSP)** — Prevents XSS, data injection, and clickjacking attacks +2. **Strict-Transport-Security (HSTS)** — Forces HTTPS connections and prevents protocol downgrade attacks +3. **X-Content-Type-Options** — Prevents MIME type sniffing attacks +4. **X-Frame-Options** — Protects against clickjacking via iframe embedding +5. **Permissions-Policy** — Disables unused browser features (camera, microphone, geolocation, etc.) +6. **Referrer-Policy** — Controls information leakage in the Referer header + +These headers are enabled with a single export in `next.config.ts`: + +```typescript +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +module.exports = createStackwrightNextConfig() +export { headers } +``` + +See [SECURITY_HEADERS.md](./SECURITY_HEADERS.md) for full documentation and customization options. + ### Layer 1: Build-Time Spec Validation (`ApprovedSpecsValidator`) Before any code is generated, the system validates: diff --git a/docs/SECURITY_HEADERS.md b/docs/SECURITY_HEADERS.md new file mode 100644 index 00000000..fb8676b9 --- /dev/null +++ b/docs/SECURITY_HEADERS.md @@ -0,0 +1,617 @@ +# Security Headers Guide + +*Practical guide to securing your Stackwright applications with HTTP security headers.* + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Quick Start](#quick-start) +3. [Default Headers](#default-headers) +4. [Customization](#customization) +5. [CSP Directives Explained](#csp-directives-explained) +6. [Migration Guide](#migration-guide) +7. [Troubleshooting](#troubleshooting) +8. [External Resources](#external-resources) + +--- + +## Overview + +Security headers are HTTP response headers that help protect your application against common web vulnerabilities. They're a critical component of **defense-in-depth** — adding layers of protection even when your code is secure. + +### Why Security Headers Matter + +| Threat | Without Headers | With Headers | +|--------|-----------------|--------------| +| XSS Attacks | Browser executes injected scripts | CSP blocks inline scripts | +| Clickjacking | Site can be embedded in iframe | `X-Frame-Options` blocks embedding | +| MIME Sniffing | Browser may execute wrong content type | `X-Content-Type-Options` stops sniffing | +| Man-in-the-Middle | HTTP connections vulnerable | HSTS forces HTTPS | +| Unwanted Tracking | Browser APIs available by default | `Permissions-Policy` disables unused features | + +### OWASP Alignment + +Stackwright's security headers implement recommendations from the [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/), which identifies these headers as essential for modern web application security. + +### Defense-in-Depth + +Security headers don't replace secure coding practices — they complement them: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────┤ +│ Input Validation │ Output Encoding │ SQL Injection │ ← Secure Code +├─────────────────────────────────────────────────────────┤ +│ CSP │ HSTS │ X-Frame-Options │ Permissions │ ← Security Headers +├─────────────────────────────────────────────────────────┤ +│ WAF │ Network Firewall │ TLS │ ← Infrastructure +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Quick Start + +Adding security headers to your Stackwright project takes just two steps: + +### 1. Update `next.config.ts` + +```typescript +// next.config.ts +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +module.exports = createStackwrightNextConfig() +export { headers } +``` + +That's it! Your application now includes all default security headers. + +### 2. Verify Headers Are Applied + +Start your dev server and check the headers: + +```bash +pnpm dev +``` + +Then in another terminal: + +```bash +curl -I http://localhost:3000 +``` + +You should see headers like: + +``` +Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; ... +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-XSS-Protection: 1; mode=block +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() +Strict-Transport-Security: max-age=31536000; includeSubDomains; preload +``` + +--- + +## Default Headers + +Stackwright enables ten security headers by default (including three cross-origin isolation headers): + +| Header | Default Value | Purpose | +|--------|---------------|---------| +| **Content-Security-Policy** | Complex CSP (see below) | Prevents XSS, clickjacking, and data injection | +| **X-Content-Type-Options** | `nosniff` | Prevents MIME type sniffing | +| **X-Frame-Options** | `DENY` | Prevents clickjacking via iframes | +| **X-XSS-Protection** | `1; mode=block` | Legacy XSS filtering (modern browsers prefer CSP) | +| **Referrer-Policy** | `strict-origin-when-cross-origin` | Controls information in the Referer header | +| **Permissions-Policy** | `camera=(), microphone=(), geolocation=(), interest-cohort=()` | Disables unused browser features | +| **Strict-Transport-Security** | `max-age=31536000; includeSubDomains; preload` | Forces HTTPS connections | +| **Cross-Origin-Opener-Policy** | `same-origin-allow-popups` | Controls browsing context group sharing | +| **Cross-Origin-Resource-Policy** | `same-origin` | Controls cross-origin resource loading | +| **Cross-Origin-Embedder-Policy** | `credentialless` | Controls cross-origin resource embedding | + +### Default Content Security Policy + +```javascript +default-src 'self'; +script-src 'self' 'unsafe-inline' 'unsafe-eval'; +style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; +font-src 'self' https://fonts.gstatic.com; +img-src 'self' data: https: blob:; +connect-src 'self'; +frame-ancestors 'none'; +object-src 'none'; +base-uri 'self'; +form-action 'self'; +upgrade-insecure-requests; +``` + +--- + +## Customization + +Stackwright's security headers are fully customizable through the `securityHeaders` option. + +### Basic Customization + +```typescript +// next.config.ts +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +const nextConfig = createStackwrightNextConfig({ + securityHeaders: { + xFrameOptions: 'SAMEORIGIN', // Allow embedding from same origin + referrerPolicy: 'no-referrer', // Don't send referrer ever + }, +}) + +module.exports = nextConfig +export { headers } +``` + +### Advanced Configuration + +```typescript +// next.config.ts +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +const nextConfig = createStackwrightNextConfig({ + securityHeaders: { + // Content Security Policy options + contentSecurityPolicy: { + reportUri: 'https://your-csp-reporter.example.com/api/csp-violations', + }, + + // Strict Transport Security + strictTransportSecurity: { + maxAge: 63072000, // 2 years (default: 1 year) + includeSubDomains: true, + preload: true, + }, + + // X-Frame-Options + xFrameOptions: 'SAMEORIGIN', + + // Referrer-Policy + referrerPolicy: 'no-referrer', + + // Permissions-Policy (control browser features) + permissionsPolicy: { + camera: ['https://example.com'], // Allow camera only for example.com + microphone: ['self'], // Allow microphone for same origin + geolocation: ['self'], + interestCohort: [], // Keep disabled + }, + }, +}) + +module.exports = nextConfig +export { headers } +``` + +### Production Configuration + +For production deployments, you may want to adjust security headers for your specific needs: + +```typescript +// next.config.ts +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +// Production recommended overrides +const nextConfig = createStackwrightNextConfig({ + securityHeaders: { + contentSecurityPolicy: { + // Override for your API domains + customDirectives: { + 'connect-src': "'self' https://api.yoursite.com wss://yoursite.com", + 'script-src': "'self' 'unsafe-inline'", // Remove unsafe-eval in production + }, + reportUri: 'https://csper.io/report/your-site-id', + }, + crossOriginOpenerPolicy: 'same-origin', + crossOriginResourcePolicy: 'same-origin', + crossOriginEmbedderPolicy: 'require-corp', + }, +}) + +module.exports = nextConfig +export { headers } +``` + +**Key production changes:** +- Remove `'unsafe-eval'` from `script-src` (or use nonce-based CSP) +- Add your API domains to `connect-src` +- Enable stricter cross-origin isolation with `same-origin` COOP +- Use `require-corp` for COEP (requires CORS/CORP on all cross-origin resources) + +> ⚠️ **Important**: Before deploying, test thoroughly! Cross-origin isolation (`COOP: same-origin` + `COEP: require-corp`) can break third-party integrations that don't support CORS. + + +### Referrer-Policy Reference + +| Value | Behavior | +|-------|----------| +| `no-referrer` | Never send referrer | +| `no-referrer-when-downgrade` | Default, send full URL unless going to HTTP | +| `origin` | Only send origin (scheme + host + port) | +| `origin-when-cross-origin` | Full URL for same origin, origin for cross-origin | +| `same-origin` | Only send when same origin | +| `strict-origin` | Origin only when protocol security level same | +| `strict-origin-when-cross-origin` | Full URL same origin, origin for equal security | +| `unsafe-url` | Always send full URL (⚠️ not recommended) | + +### Permissions-Policy Values + +Each feature can be: +- `()` — Completely disabled +- `(self)` — Enabled only for same-origin +- `('self')` — Same as `self` +- `('https://example.com')` — Enabled for specific origin +- `('self' 'https://example.com')` — Multiple origins + + +### Cross-Origin Isolation Headers + +Stackwright includes three cross-origin isolation headers that enhance security by controlling how your pages interact with cross-origin content. + +#### Cross-Origin-Opener-Policy (COOP) + +Controls whether the document shares its browsing context group with cross-origin documents. + +| Value | Behavior | +|-------|----------| +| `same-origin` | Full isolation — blocks cross-origin windows from accessing this document | +| `same-origin-allow-popups` | **Default** — allows popups but keeps document isolated | +| `unsafe-none` | Allows sharing with cross-origin documents (reduces isolation) | + +**Note**: Setting `same-origin` enables cross-origin isolation, which is required for: +- `SharedArrayBuffer` (threading) +- `performance.measureMemory()` API +- `Navigator.share()` with arbitrary files + +#### Cross-Origin-Resource-Policy (CORP) + +Controls which cross-origin requests can load resources from your pages. + +| Value | Behavior | +|-------|----------| +| `same-origin` | **Default** — only same-origin requests can load resources | +| `same-site` | Same-site requests (including cross-origin subdomains) can load | +| `cross-origin` | Any cross-origin request can load (use only for public resources) | + +#### Cross-Origin-Embedder-Policy (COEP) + +Controls whether the document can load cross-origin resources without explicit permission. + +| Value | Behavior | +|-------|----------| +| `credentialless` | **Default** — cross-origin resources load without credentials, or with explicit permission | +| `require-corp` | Cross-origin resources must have CORS or CORP headers to load | +| `no-cors` | Allows loading cross-origin resources without CORS (reduces security) | + +**Note**: COEP combined with COOP `same-origin` enables **cross-origin isolation**, unlocking advanced browser features. + + + +--- + +## CSP Directives Explained + +The Content Security Policy is the most powerful security header. Here's what each directive does: + +### Source Values + +| Value | Meaning | +|-------|---------| +| `'self'` | Same origin (protocol + host + port) | +| `'none'` | Block everywhere | +| `'unsafe-inline'` | Allow inline scripts/styles (⚠️ weakens CSP) | +| `'unsafe-eval'` | Allow `eval()` and similar (⚠️ security risk) | +| `data:` | Data URLs (base64 images, etc.) | +| `https:` | Any HTTPS URL | +| `'nonce-xxx'` | Allow specific inline script with nonce | +| `'strict-dynamic'` | Trust scripts loaded by already-trusted scripts | + +### Directive Reference + +| Directive | Default in Stackwright | Purpose | +|-----------|------------------------|---------| +| `default-src` | `'self'` | Fallback for unspecified types | +| `script-src` | `'self' 'unsafe-inline' 'unsafe-eval'` | JavaScript sources | +| `style-src` | `'self' 'unsafe-inline' https://fonts.googleapis.com` | Stylesheet sources | +| `font-src` | `'self' https://fonts.gstatic.com` | Font sources | +| `img-src` | `'self' data: https: blob:` | Image sources | +| `connect-src` | `'self'` | XHR, fetch, WebSocket origins | +| `frame-src` | *(inherited from frame-ancestors)* | Iframe sources | +| `frame-ancestors` | `'none'` | Who can embed this page | +| `object-src` | `'none'` | Plugin sources (Flash, Java, etc.) | +| `base-uri` | `'self'` | Valid values for `` element | +| `form-action` | `'self'` | Form submission targets | +| `upgrade-insecure-requests` | enabled | Auto-upgrade HTTP to HTTPS | + +### ⚠️ Security Tradeoffs + +> **Note on `unsafe-inline` and `unsafe-eval`**: These directives are included for Next.js compatibility out of the box. For production deployments handling sensitive data, consider: +> - Using Next.js's built-in nonce support for inline scripts +> - Using hash-based CSP allowlists for known scripts +> - See the Nonce-Based CSP example below + +#### Why `unsafe-inline`? + +Next.js requires `'unsafe-inline'` for: +- Component-level styles +- Dynamic styles via CSS-in-JS +- React's internal mechanisms + +#### Why `unsafe-eval`? + +Required for: +- Some older webpack loaders +- Dynamic code execution patterns +- Certain debugging tools + +If you don't use these patterns, you can create a custom CSP without them. + +### Nonce-Based CSP (Advanced) + +For stricter CSP in production, use Next.js's built-in nonce support: + +```typescript +// next.config.ts +const { createStackwrightNextConfig, headers } = require('@stackwright/nextjs') + +const nextConfig = createStackwrightNextConfig({ + security: { + generateNonce: true, + }, +}) + +module.exports = nextConfig +export { headers } +``` + +Then in your root layout: + +```tsx +import { getCspNonce } from 'next/navigation' + +export default function RootLayout({ children }) { + const nonce = getCspNonce() + + return ( + + +