From 25686c0feed92cbd0b709c3f5da802597abcb7ff Mon Sep 17 00:00:00 2001 From: Adam Lane Date: Sun, 2 Nov 2025 12:39:51 -0800 Subject: [PATCH 1/6] fix: remove broken createNonceGetter() - v1.0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Remove createNonceGetter() due to AsyncLocalStorage bug ## What Changed - **Removed:** createNonceGetter() function (broken isomorphic wrapper) - **Updated:** src/nonce.ts with deprecation notice and correct pattern - **Updated:** src/index.ts exports (removed createNonceGetter export) - **Updated:** README with v1.0.1 pattern (direct context access) - **Added:** Comprehensive migration guide (docs/MIGRATION-1.0-to-1.0.1.md) - **Added:** Changeset for v1.0.1 release ## Why This Change? The createIsomorphicFn() wrapper broke AsyncLocalStorage context chain: - Server getStartContext() failed with "No Start context found" - Scripts rendered without nonce attributes - All scripts blocked by CSP ## Fixed by Using direct context access (official TanStack pattern): ```typescript export async function getRouter() { let nonce: string | undefined; if (typeof window === 'undefined') { const { getStartContext } = await import('@tanstack/start-storage-context'); nonce = getStartContext().contextAfterGlobalMiddlewares?.nonce; } return createRouter({ ssr: { nonce } }); } ``` ## Migration See docs/MIGRATION-1.0-to-1.0.1.md for complete guide. ## Quality Checks ✅ TypeScript compilation - PASSED ✅ Linting (Biome) - PASSED ✅ Tests (28 tests) - PASSED Reference: https://github.com/TanStack/router/discussions/3028 --- .../fix-remove-createNonceGetter-v1.0.1.md | 54 +++ README.md | 85 +++-- docs/MIGRATION-1.0-to-1.0.1.md | 355 ++++++++++++++++++ src/index.ts | 4 +- src/nonce.ts | 52 ++- 5 files changed, 492 insertions(+), 58 deletions(-) create mode 100644 .changeset/fix-remove-createNonceGetter-v1.0.1.md create mode 100644 docs/MIGRATION-1.0-to-1.0.1.md diff --git a/.changeset/fix-remove-createNonceGetter-v1.0.1.md b/.changeset/fix-remove-createNonceGetter-v1.0.1.md new file mode 100644 index 0000000..1bed490 --- /dev/null +++ b/.changeset/fix-remove-createNonceGetter-v1.0.1.md @@ -0,0 +1,54 @@ +--- +'@enalmada/start-secure': patch +--- + +**BREAKING CHANGE:** Remove broken `createNonceGetter()` function + +## What Changed + +- **Removed:** `createNonceGetter()` function (had critical AsyncLocalStorage bug) +- **Updated:** README with official TanStack pattern using direct context access +- **Updated:** API documentation to show deprecation notice +- **Added:** Comprehensive migration guide (docs/MIGRATION-1.0-to-1.0.1.md) + +## Why This Change? + +The `createIsomorphicFn()` wrapper in `createNonceGetter()` broke Node.js AsyncLocalStorage context chain: +- Server-side `getStartContext()` failed with "No Start context found" +- Scripts rendered without nonce attributes +- All scripts blocked by CSP + +## Migration Required + +**Before (v1.0.0 - BROKEN):** +```typescript +import { createNonceGetter } from '@enalmada/start-secure'; +const getNonce = createNonceGetter(); +const router = createRouter({ ssr: { nonce: getNonce() } }); +``` + +**After (v1.0.1 - WORKING):** +```typescript +export async function getRouter() { + let nonce: string | undefined; + if (typeof window === 'undefined') { + const { getStartContext } = await import('@tanstack/start-storage-context'); + nonce = getStartContext().contextAfterGlobalMiddlewares?.nonce; + } + return createRouter({ ssr: { nonce } }); +} +``` + +This aligns with the official TanStack Router pattern: https://github.com/TanStack/router/discussions/3028 + +## What Still Works + +No changes to these (all work perfectly): +- ✅ `createCspMiddleware()` - Middleware nonce generation +- ✅ `generateNonce()` - Crypto-random nonce generation +- ✅ `buildCspHeader()` - CSP header building +- ✅ All security headers and CSP rules + +## Full Migration Guide + +See [docs/MIGRATION-1.0-to-1.0.1.md](../docs/MIGRATION-1.0-to-1.0.1.md) for complete migration guide with troubleshooting. diff --git a/README.md b/README.md index aa59392..07f6aeb 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,29 @@ Security header management for TanStack Start applications with native nonce sup - 📝 Rule descriptions for documentation - 🔐 **Native per-request nonce generation** (v0.2+) - ⚡ **Middleware pattern** for TanStack Start (v0.2+) -- 🌐 **Isomorphic nonce access** (server + client) -- 🚀 Minimal setup (~10 lines) +- 🎯 **Official TanStack pattern** (direct context access, v1.0.1+) +- 🚀 Minimal setup (~20 lines) -## What's New in v0.2 +## What's New in v1.0.1 -TanStack Start now has **native nonce support** via `router.options.ssr.nonce`. This package has been updated to provide: +**BREAKING CHANGE:** Removed `createNonceGetter()` due to critical AsyncLocalStorage bug. + +**Why the change?** +- The isomorphic wrapper broke AsyncLocalStorage context chain +- Scripts were rendered without nonce attributes +- Fixed by using direct context access (official TanStack pattern) + +**Migration:** See [MIGRATION-1.0-to-1.0.1.md](./docs/MIGRATION-1.0-to-1.0.1.md) + +## What's New in v1.0.0 (Previously v0.2) + +TanStack Start has **native nonce support** via `router.options.ssr.nonce`. This package provides: - **Per-request nonce generation** - Unique cryptographic nonce for each request - **Middleware pattern** - Integrates with TanStack Start's global middleware system -- **Isomorphic nonce getter** - Works seamlessly on server and client - **No `'unsafe-inline'` for scripts** - Strict CSP in production (scripts only, styles remain pragmatic) - **Automatic nonce application** - TanStack router applies nonces to all framework scripts +- **Direct context access** - Official TanStack pattern (v1.0.1+) **Reference:** [TanStack Router Discussion #3028](https://github.com/TanStack/router/discussions/3028) @@ -35,7 +46,7 @@ TanStack Start now has **native nonce support** via `router.options.ssr.nonce`. bun add @enalmada/start-secure ``` -## Quick Start (v0.2 - Recommended) +## Quick Start (v1.0.1 - Recommended) ### Step 1: Create CSP rules configuration @@ -84,24 +95,34 @@ export const startInstance = createStart(() => ({ ```typescript import { createRouter } from '@tanstack/react-router'; -import { createNonceGetter } from '@enalmada/start-secure'; -const getNonce = createNonceGetter(); +export async function getRouter() { + // Get nonce on server (client uses meta tag automatically) + let nonce: string | undefined; + + if (typeof window === 'undefined') { + // Dynamic import for server-only code + const { getStartContext } = await import('@tanstack/start-storage-context'); + const context = getStartContext(); + nonce = context.contextAfterGlobalMiddlewares?.nonce; + } -export function getRouter() { const router = createRouter({ routeTree, // ... other options - ssr: { - nonce: getNonce() // Applies nonce to all framework scripts - } + ssr: { nonce } // Applies nonce to all framework scripts }); return router; } ``` -That's it! **Total setup: ~10 lines of code.** +**Why this pattern?** +- Direct context access (official TanStack pattern) +- No wrapper to break AsyncLocalStorage +- Works on both server and client + +That's it! **Total setup: ~20 lines of code.** ## API Reference @@ -131,21 +152,25 @@ const middleware = createCspMiddleware({ }); ``` -#### `createNonceGetter()` +#### `createNonceGetter()` ⚠️ REMOVED in v1.0.1 -Creates an isomorphic function that retrieves the nonce on both server and client. +**This function has been removed due to a critical AsyncLocalStorage bug.** -**Server behavior:** Retrieves nonce from TanStack Start middleware context -**Client behavior:** Retrieves nonce from `` tag +The isomorphic wrapper broke AsyncLocalStorage context chain, preventing nonce access. +Use direct context access instead (see Quick Start above). -**Returns:** Isomorphic function that returns the current nonce +**Migration:** See [MIGRATION-1.0-to-1.0.1.md](./docs/MIGRATION-1.0-to-1.0.1.md) -**Example:** +**Correct pattern (v1.0.1+):** ```typescript -import { createNonceGetter } from '@enalmada/start-secure'; - -const getNonce = createNonceGetter(); -const router = createRouter({ ssr: { nonce: getNonce() } }); +export async function getRouter() { + let nonce: string | undefined; + if (typeof window === 'undefined') { + const { getStartContext } = await import('@tanstack/start-storage-context'); + nonce = getStartContext().contextAfterGlobalMiddlewares?.nonce; + } + return createRouter({ ssr: { nonce } }); +} ``` #### `generateNonce()` @@ -383,11 +408,15 @@ export const startInstance = createStart(() => ({ ] })); -// src/router.tsx (UPDATED) -import { createNonceGetter } from '@enalmada/start-secure'; - -const getNonce = createNonceGetter(); -const router = createRouter({ ssr: { nonce: getNonce() } }); +// src/router.tsx (UPDATED - v1.0.1) +export async function getRouter() { + let nonce: string | undefined; + if (typeof window === 'undefined') { + const { getStartContext } = await import('@tanstack/start-storage-context'); + nonce = getStartContext().contextAfterGlobalMiddlewares?.nonce; + } + return createRouter({ ssr: { nonce } }); +} // src/server.ts (SIMPLIFIED) const fetch = createStartHandler(defaultStreamHandler); diff --git a/docs/MIGRATION-1.0-to-1.0.1.md b/docs/MIGRATION-1.0-to-1.0.1.md new file mode 100644 index 0000000..f5e3d8f --- /dev/null +++ b/docs/MIGRATION-1.0-to-1.0.1.md @@ -0,0 +1,355 @@ +# Migration Guide: v1.0.0 to v1.0.1 + +## Breaking Change: createNonceGetter() Removed + +### Why This Change? + +v1.0.0's `createNonceGetter()` had a critical bug where the isomorphic wrapper broke AsyncLocalStorage, preventing nonce access. v1.0.1 removes this broken function and aligns with the official TanStack Router pattern. + +**The Bug:** +- `createIsomorphicFn()` wrapper broke AsyncLocalStorage context chain +- Server-side `getStartContext()` failed with "No Start context found" +- Scripts rendered without nonce attributes +- All scripts blocked by CSP + +**The Fix:** +- Remove broken isomorphic wrapper +- Use direct context access (official TanStack pattern) +- Simpler, more explicit, and actually works + +## Migration Steps + +### Before (v1.0.0 - BROKEN) + +```typescript +// src/router.tsx +import { createNonceGetter } from '@enalmada/start-secure'; + +const getNonce = createNonceGetter(); // ❌ Broken - returns undefined + +export function getRouter() { + return createRouter({ + ssr: { nonce: getNonce() } // ❌ Scripts have no nonces + }); +} +``` + +### After (v1.0.1 - WORKING) + +```typescript +// src/router.tsx +import { createRouter } from '@tanstack/react-router'; + +export async function getRouter() { + // Get nonce on server (client uses meta tag automatically) + let nonce: string | undefined; + + if (typeof window === 'undefined') { + // Dynamic import for server-only code + const { getStartContext } = await import('@tanstack/start-storage-context'); + const context = getStartContext(); + nonce = context.contextAfterGlobalMiddlewares?.nonce; + } + + return createRouter({ + // ... other options + ssr: { nonce } // ✅ Scripts now have nonces + }); +} +``` + +## Step-by-Step Migration + +### 1. Update Package + +```bash +bun add @enalmada/start-secure@^1.0.1 +``` + +### 2. Update Router Code + +**Change 1:** Make `getRouter()` async + +```diff +- export function getRouter() { ++ export async function getRouter() { +``` + +**Change 2:** Remove `createNonceGetter()` import and usage + +```diff +- import { createNonceGetter } from '@enalmada/start-secure'; +- const getNonce = createNonceGetter(); +``` + +**Change 3:** Add direct context access + +```typescript +// At the start of getRouter() +let nonce: string | undefined; + +if (typeof window === 'undefined') { + // Dynamic import prevents Node.js code in browser bundle + const { getStartContext } = await import('@tanstack/start-storage-context'); + const context = getStartContext(); + nonce = context.contextAfterGlobalMiddlewares?.nonce; +} +``` + +**Change 4:** Update router config + +```diff + return createRouter({ + // ... other options + ssr: { +- nonce: getNonce() ++ nonce + } + }); +``` + +Or, if using `exactOptionalPropertyTypes`: + +```diff + return createRouter({ + // ... other options +- ssr: { +- nonce: getNonce() +- } ++ ...(nonce ? { ssr: { nonce } } : {}) + }); +``` + +### 3. Verify Integration + +After updating: + +1. **Start dev server:** + ```bash + bun dev + ``` + +2. **Open browser DevTools** + +3. **Check Console** - Should see no CSP violations + +4. **Inspect Page Source** (View → Developer → View Source) + - Search for `` exists in `` + +5. **Check Network Tab** - Response Headers + - Verify CSP header includes nonce value + - Example: `Content-Security-Policy: script-src 'nonce-XXX' 'strict-dynamic'` + +## Complete Example + +### Full Router Implementation + +```typescript +// src/router.tsx +import { i18n } from "@lingui/core"; +import { I18nProvider } from "@lingui/react"; +import { QueryClient } from "@tanstack/react-query"; +import { createRouter } from "@tanstack/react-router"; +import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"; +import type { ReactNode } from "react"; +import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"; +import { NotFound } from "./components/NotFound"; +import { routeTree } from "./routeTree.gen"; + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} + +interface RouterContext { + queryClient: QueryClient; +} + +export async function getRouter() { + // Get nonce on server (client uses meta tag automatically) + let nonce: string | undefined; + + if (typeof window === "undefined") { + try { + // Dynamic import for server-only code + const { getStartContext } = await import("@tanstack/start-storage-context"); + const context = getStartContext(); + nonce = context.contextAfterGlobalMiddlewares?.nonce; + } catch (error) { + // Context not available (shouldn't happen, but handle gracefully) + nonce = undefined; + } + } + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // 5 minutes + }, + }, + }); + + const router = createRouter({ + routeTree, + context: { queryClient } as RouterContext, + defaultPreload: "intent", + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: NotFound, + Wrap: ({ children }: { children: ReactNode }) => ( + {children} + ), + + // CSP nonce support - applies to all framework-generated scripts + ...(nonce ? { ssr: { nonce } } : {}), + }); + + setupRouterSsrQueryIntegration({ + router, + queryClient, + handleRedirects: true, + wrapQueryClient: true, + }); + + return router; +} +``` + +### Middleware Setup (No Changes) + +The middleware setup remains unchanged from v1.0.0: + +```typescript +// src/start.ts +import { createStart } from '@tanstack/react-start'; +import { createCspMiddleware } from '@enalmada/start-secure'; +import { cspRules } from './config/cspRules'; + +export const startInstance = createStart(() => ({ + requestMiddleware: [ + createCspMiddleware({ + rules: cspRules, + options: { isDev: process.env.NODE_ENV !== 'production' } + }) + ] +})); +``` + +## What Still Works + +These parts of the package are **NOT** affected by this change: + +- ✅ `createCspMiddleware()` - Works perfectly +- ✅ `generateNonce()` - Works perfectly +- ✅ `buildCspHeader()` - Works perfectly +- ✅ Middleware nonce generation - Works perfectly +- ✅ CSP header setting - Works perfectly + +Only the **router integration pattern** changed. The middleware functionality remains solid. + +## Troubleshooting + +### Scripts Still Blocked by CSP + +**Symptom:** +``` +Content-Security-Policy: The page's settings blocked an inline script +``` + +**Check:** +1. Is `getRouter()` async? (Required for dynamic import) +2. Is dynamic import inside `typeof window === 'undefined'` check? +3. Are you accessing `contextAfterGlobalMiddlewares?.nonce`? +4. Is nonce passed to router config? + +**Debug:** +Add console.log to verify nonce value: +```typescript +if (typeof window === 'undefined') { + const { getStartContext } = await import('@tanstack/start-storage-context'); + const context = getStartContext(); + nonce = context.contextAfterGlobalMiddlewares?.nonce; + console.log('[DEBUG] Nonce from context:', nonce ? 'EXISTS' : 'UNDEFINED'); +} +``` + +### TypeScript Error with exactOptionalPropertyTypes + +**Error:** +``` +Type 'string | undefined' is not assignable to type 'string' +``` + +**Fix:** +Use conditional spread instead of always including ssr object: +```typescript +return createRouter({ + // ... other options + ...(nonce ? { ssr: { nonce } } : {}) +}); +``` + +### Import Error in Browser + +**Error:** +``` +Module "node:async_hooks" has been externalized for browser compatibility +``` + +**Cause:** Imported `getStartContext` at module level (browser tries to load it) + +**Fix:** Use dynamic import inside server-only conditional: +```typescript +if (typeof window === 'undefined') { + const { getStartContext } = await import('@tanstack/start-storage-context'); + // ... +} +``` + +## Why This Pattern is Better + +### Official TanStack Pattern + +This migration aligns with the official TanStack Router pattern documented in: +- [Router Discussion #3028](https://github.com/TanStack/router/discussions/3028) + +The TanStack maintainers recommend this exact approach. We were trying to be "helpful" with an isomorphic wrapper, but it broke AsyncLocalStorage. + +### Benefits of Direct Access + +1. **Works** - No AsyncLocalStorage bugs +2. **Simple** - 5 lines of code, very explicit +3. **Official** - Matches TanStack documentation +4. **Debuggable** - Easy to add console.log for troubleshooting +5. **No magic** - No hidden wrapper behavior + +### AsyncLocalStorage Explained + +**Common Question:** "Does AsyncLocalStorage work in browsers?" + +**Answer:** AsyncLocalStorage is a **Node.js server-side API only**. It has nothing to do with browser compatibility. + +**How it works:** +- **Server (Node.js):** Uses AsyncLocalStorage to track per-request context +- **Client (Browser):** Reads nonce from `` tag + +The workaround is needed because `createIsomorphicFn()` broke the **server-side** AsyncLocalStorage chain, not because of any browser issue. + +## Support + +If you encounter issues after migration: + +1. Check this guide's Troubleshooting section +2. Verify you're on v1.0.1 or later +3. Review the Complete Example above +4. Check [GitHub Issues](https://github.com/Enalmada/start-secure/issues) + +## References + +- **Official TanStack Pattern:** https://github.com/TanStack/router/discussions/3028 +- **Bug Analysis:** See TanStarter `.plan/plans/tanstack_csp/CRITICAL-BUG.md` +- **AsyncLocalStorage Docs:** https://nodejs.org/api/async_context.html +- **Package Repo:** https://github.com/Enalmada/start-secure diff --git a/src/index.ts b/src/index.ts index 31a16a1..d5d9d7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,4 +23,6 @@ export type { export type { CspMiddlewareConfig } from "./middleware"; // New v0.2 API - Middleware pattern with per-request nonces export { createCspMiddleware } from "./middleware"; -export { createNonceGetter, generateNonce } from "./nonce"; +// Note: createNonceGetter removed in v1.0.1 due to AsyncLocalStorage bug +// Use direct context access instead (see docs/MIGRATION-1.0-to-1.0.1.md) +export { generateNonce } from "./nonce"; diff --git a/src/nonce.ts b/src/nonce.ts index 82ced83..0ebd97a 100644 --- a/src/nonce.ts +++ b/src/nonce.ts @@ -1,11 +1,8 @@ /** - * Nonce generation and retrieval utilities - * Provides cryptographically secure nonce generation and isomorphic nonce access + * Nonce generation utilities + * Provides cryptographically secure nonce generation for CSP middleware */ -import { createIsomorphicFn } from "@tanstack/react-start"; -import { getStartContext } from "@tanstack/start-storage-context"; - /** * Generate cryptographically secure random nonce for CSP * @@ -25,36 +22,33 @@ export function generateNonce(): string { } /** - * Create isomorphic function to get nonce on both server and client - * - * Server: Retrieves from TanStack Start global middleware context - * Client: Retrieves from meta tag (auto-created by TanStack Start) + * REMOVED: createNonceGetter() - Had critical AsyncLocalStorage bug * - * @returns Function that retrieves nonce from appropriate context + * The isomorphic wrapper broke AsyncLocalStorage context chain, preventing + * access to middleware nonce. Use direct context access instead: * * @example * ```typescript * import { createRouter } from '@tanstack/react-router'; - * import { createNonceGetter } from '@enalmada/start-secure'; * - * const getNonce = createNonceGetter(); - * - * const router = createRouter({ - * // ... other options - * ssr: { - * nonce: getNonce() // Applies nonce to all and + * export async function getRouter() { + * // Get nonce on server (client uses meta tag automatically) + * let nonce: string | undefined; + * if (typeof window === 'undefined') { + * const { getStartContext } = await import('@tanstack/start-storage-context'); + * const context = getStartContext(); + * nonce = context.contextAfterGlobalMiddlewares?.nonce; * } - * }); + * + * return createRouter({ + * // ... other options + * ssr: { nonce } // TanStack Start applies to all framework scripts + * }); + * } * ``` + * + * This follows the official TanStack Router pattern: + * https://github.com/TanStack/router/discussions/3028 + * + * See docs/MIGRATION-1.0-to-1.0.1.md for migration guide. */ -export function createNonceGetter() { - return createIsomorphicFn() - .server(() => { - const context = getStartContext(); - return context.contextAfterGlobalMiddlewares?.nonce; - }) - .client(() => { - const meta = document.querySelector("meta[property='csp-nonce']"); - return meta?.getAttribute("content") ?? undefined; - }); -} From 757edf1c550fb1a98105e54085c30994513fe376 Mon Sep 17 00:00:00 2001 From: Adam Lane Date: Sun, 2 Nov 2025 12:42:04 -0800 Subject: [PATCH 2/6] docs: remove version numbers from README for maintainability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed all specific version references (v0.1, v0.2, v1.0.0, v1.0.1) from README. Makes documentation version-agnostic and easier to maintain. Changes: - Replaced version-specific sections with descriptive names - 'v0.2 API' → 'Middleware API' - 'v0.1' → 'Handler Wrapper - Deprecated' - Removed version numbers from features list - Updated migration section titles - Kept migration guide link (still useful for users upgrading) README now focuses on current best practices without version coupling. --- README.md | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 07f6aeb..c2f2618 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,12 @@ Security header management for TanStack Start applications with native nonce sup - 🔄 Automatic CSP rule merging and deduplication - 🛠️ Development mode support (HMR, eval, WebSocket) - 📝 Rule descriptions for documentation -- 🔐 **Native per-request nonce generation** (v0.2+) -- ⚡ **Middleware pattern** for TanStack Start (v0.2+) -- 🎯 **Official TanStack pattern** (direct context access, v1.0.1+) +- 🔐 **Native per-request nonce generation** +- ⚡ **Middleware pattern** for TanStack Start +- 🎯 **Official TanStack pattern** (direct context access) - 🚀 Minimal setup (~20 lines) -## What's New in v1.0.1 - -**BREAKING CHANGE:** Removed `createNonceGetter()` due to critical AsyncLocalStorage bug. - -**Why the change?** -- The isomorphic wrapper broke AsyncLocalStorage context chain -- Scripts were rendered without nonce attributes -- Fixed by using direct context access (official TanStack pattern) - -**Migration:** See [MIGRATION-1.0-to-1.0.1.md](./docs/MIGRATION-1.0-to-1.0.1.md) - -## What's New in v1.0.0 (Previously v0.2) +## Overview TanStack Start has **native nonce support** via `router.options.ssr.nonce`. This package provides: @@ -36,7 +25,7 @@ TanStack Start has **native nonce support** via `router.options.ssr.nonce`. This - **Middleware pattern** - Integrates with TanStack Start's global middleware system - **No `'unsafe-inline'` for scripts** - Strict CSP in production (scripts only, styles remain pragmatic) - **Automatic nonce application** - TanStack router applies nonces to all framework scripts -- **Direct context access** - Official TanStack pattern (v1.0.1+) +- **Direct context access** - Official TanStack pattern (no broken wrappers) **Reference:** [TanStack Router Discussion #3028](https://github.com/TanStack/router/discussions/3028) @@ -46,7 +35,7 @@ TanStack Start has **native nonce support** via `router.options.ssr.nonce`. This bun add @enalmada/start-secure ``` -## Quick Start (v1.0.1 - Recommended) +## Quick Start ### Step 1: Create CSP rules configuration @@ -126,7 +115,7 @@ That's it! **Total setup: ~20 lines of code.** ## API Reference -### v0.2 API (Recommended) +### Middleware API (Recommended) #### `createCspMiddleware(config)` @@ -152,7 +141,7 @@ const middleware = createCspMiddleware({ }); ``` -#### `createNonceGetter()` ⚠️ REMOVED in v1.0.1 +#### `createNonceGetter()` ⚠️ REMOVED **This function has been removed due to a critical AsyncLocalStorage bug.** @@ -161,7 +150,7 @@ Use direct context access instead (see Quick Start above). **Migration:** See [MIGRATION-1.0-to-1.0.1.md](./docs/MIGRATION-1.0-to-1.0.1.md) -**Correct pattern (v1.0.1+):** +**Correct pattern:** ```typescript export async function getRouter() { let nonce: string | undefined; @@ -375,11 +364,11 @@ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload (product Permissions-Policy: camera=(), microphone=(), geolocation=(), ... ``` -## Migration from v0.1 +## Migration from Handler Wrapper Pattern If you're using the old `createSecureHandler` API, here's how to migrate: -### Before (v0.1) +### Before (Handler Wrapper - Deprecated) ```typescript // src/server.ts @@ -395,7 +384,7 @@ export default { }; ``` -### After (v0.2) +### After (Middleware Pattern - Recommended) ```typescript // src/start.ts (NEW FILE) @@ -408,7 +397,7 @@ export const startInstance = createStart(() => ({ ] })); -// src/router.tsx (UPDATED - v1.0.1) +// src/router.tsx (UPDATED) export async function getRouter() { let nonce: string | undefined; if (typeof window === 'undefined') { @@ -422,7 +411,7 @@ export async function getRouter() { const fetch = createStartHandler(defaultStreamHandler); ``` -### Benefits of v0.2 +### Benefits of Middleware Pattern - ✅ Per-request nonce generation (not static) - ✅ No `'unsafe-inline'` for scripts in production @@ -432,9 +421,9 @@ const fetch = createStartHandler(defaultStreamHandler); --- -## Legacy API (v0.1) +## Legacy API (Handler Wrapper) -The v0.1 handler wrapper API is still available for backward compatibility but is **deprecated**. Please migrate to v0.2 for better security. +The old handler wrapper API is still available for backward compatibility but is **deprecated**. Please migrate to the middleware pattern for better security. ### `createSecureHandler(config)` (Deprecated) From ffdaf97ccd7fec9cb7e7b7c541a030db2ae83e34 Mon Sep 17 00:00:00 2001 From: Adam Lane Date: Sun, 2 Nov 2025 12:48:09 -0800 Subject: [PATCH 3/6] fix: remove redundant CSP directives to eliminate console warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean CSP Level 3 implementation that removes directives ignored by browsers with 'strict-dynamic', eliminating 13 console warnings. Changes: - src/internal/csp-builder.ts: Clean script-src directives - Remove 'self', 'unsafe-inline', https:, http: (ignored with strict-dynamic) - Keep only: nonce, strict-dynamic, unsafe-eval (dev only) - src/internal/defaults.ts: Matching clean defaults - Same pattern as csp-builder for consistency - Fallback to old pattern only if no nonce provided Why these directives are ignored: - 'self' - Ignored with 'strict-dynamic' (use nonce-based trust) - 'unsafe-inline' - Ignored when nonce is present (CSP Level 2+) - https:/http: - Ignored with 'strict-dynamic' (overly permissive) - URL whitelists - Ignored with 'strict-dynamic' (use nonce chain) Result: - 13 CSP warnings → 0 warnings - Cleaner, standards-compliant CSP - No loss of functionality (directives were already ignored) Reference: CSP Level 3 spec - strict-dynamic behavior --- src/internal/csp-builder.ts | 37 +++++++++++++++++++------------------ src/internal/defaults.ts | 9 ++++++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/internal/csp-builder.ts b/src/internal/csp-builder.ts index a2f98cc..a5b3b3a 100644 --- a/src/internal/csp-builder.ts +++ b/src/internal/csp-builder.ts @@ -31,24 +31,25 @@ export function buildCspHeader(rules: CspRule[], nonce: string, isDev: boolean): "manifest-src": ["'self'"], "media-src": ["'self'"], "object-src": ["'none'"], - // Script sources with nonce - // Note: 'unsafe-inline' is ignored when nonce is present (CSP Level 2+) - // It's included for backward compatibility with older browsers - "script-src": [ - "'self'", - `'nonce-${nonce}'`, - "'unsafe-inline'", - "'strict-dynamic'", - ...(isDev ? ["'unsafe-eval'", "https:", "http:"] : []), - ], - // Allow