npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@enalmada/start-secure

v1.0.2

Published

Security header management for TanStack Start

Readme

@enalmada/start-secure

Security header management for TanStack Start applications with native nonce support.

npm version License: MIT

Features

  • 🔒 Secure defaults (strict CSP, security headers)
  • 🎯 Type-safe CSP rule definitions
  • 🔄 Automatic CSP rule merging and deduplication
  • 🛠️ Development mode support (HMR, eval, WebSocket)
  • 📝 Rule descriptions for documentation
  • 🔐 Native per-request nonce generation
  • Middleware pattern for TanStack Start
  • 🎯 Official TanStack pattern (direct context access)
  • 🚀 Minimal setup (~20 lines)

Overview

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
  • 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 (no broken wrappers)

Reference: TanStack Router Discussion #3028

Installation

bun add @enalmada/start-secure

Quick Start

Step 1: Create CSP rules configuration

File: src/config/cspRules.ts

import type { CspRule } from '@enalmada/start-secure';

export const cspRules: CspRule[] = [
  {
    description: 'google-auth',
    'form-action': "'self' https://accounts.google.com",
    'img-src': "https://*.googleusercontent.com",
    'connect-src': "https://*.googleusercontent.com",
  },
  {
    description: 'posthog-analytics',
    'script-src': "https://*.posthog.com",
    'connect-src': "https://*.posthog.com",
  },
];

Step 2: Register CSP middleware

File: 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' }
    })
  ]
}));

Step 3: Configure router with nonce

File: 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;
  }

  const router = createRouter({
    routeTree,
    // ... other options
    ssr: { nonce }  // Applies nonce to all framework scripts
  });

  return router;
}

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

Middleware API (Recommended)

createCspMiddleware(config)

Creates CSP middleware for TanStack Start with per-request nonce generation.

Parameters:

  • config.rules?: CspRule[] - Array of CSP rules to merge with defaults
  • config.options.isDev?: boolean - Enable development mode (WebSocket, unsafe-eval, HTTPS/HTTP sources)
  • config.nonceGenerator?: () => string - Custom nonce generator (optional, defaults to crypto-random)
  • config.additionalHeaders?: Record<string, string> - Additional response headers to set

Returns: TanStack Start middleware

Example:

import { createCspMiddleware } from '@enalmada/start-secure';

const middleware = createCspMiddleware({
  rules: [
    { description: 'google-fonts', 'font-src': 'https://fonts.gstatic.com' }
  ],
  options: { isDev: process.env.NODE_ENV !== 'production' }
});

createNonceGetter() ⚠️ REMOVED

This function has been removed due to a critical AsyncLocalStorage bug.

The isomorphic wrapper broke AsyncLocalStorage context chain, preventing nonce access. Use direct context access instead (see Quick Start above).

Migration: See MIGRATION-1.0-to-1.0.1.md

Correct pattern:

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()

Generates a cryptographically secure random nonce for CSP.

Returns: Base64-encoded random nonce (128-bit from UUID)

Example:

import { generateNonce } from '@enalmada/start-secure';

const nonce = generateNonce();
// "Y2QxMjM0NTY3ODkwMTIzNDU2Nzg="

buildCspHeader(rules, nonce, isDev)

Low-level utility to build CSP header string from rules and nonce.

Parameters:

  • rules: CspRule[] - CSP rules to merge
  • nonce: string - Nonce for this request
  • isDev: boolean - Whether in development mode

Returns: CSP header string

Example:

import { buildCspHeader } from '@enalmada/start-secure';

const csp = buildCspHeader(rules, generateNonce(), false);
// "default-src 'self'; script-src 'self' 'nonce-...' ..."

Types

CspRule

interface CspRule {
  description?: string; // Document why this rule exists
  source?: string;      // Reserved for future use

  // CSP directives - all optional, support both string and string[]
  'base-uri'?: string | string[];
  'child-src'?: string | string[];
  'connect-src'?: string | string[];
  'default-src'?: string | string[];
  'font-src'?: string | string[];
  'form-action'?: string | string[];
  'frame-ancestors'?: string | string[];
  'frame-src'?: string | string[];
  'img-src'?: string | string[];
  'manifest-src'?: string | string[];
  'media-src'?: string | string[];
  'object-src'?: string | string[];
  'script-src'?: string | string[];
  'script-src-attr'?: string | string[];
  'script-src-elem'?: string | string[];
  'style-src'?: string | string[];
  'style-src-attr'?: string | string[];
  'style-src-elem'?: string | string[];
  'worker-src'?: string | string[];
}

CspMiddlewareConfig

interface CspMiddlewareConfig {
  rules?: CspRule[];
  options?: SecurityOptions;
  nonceGenerator?: () => string;
  additionalHeaders?: Record<string, string>;
}

Security Model

Scripts: Strict Nonce-based CSP

Production:

script-src 'nonce-XXX' 'strict-dynamic'
script-src-elem 'nonce-XXX' 'strict-dynamic'
  • ✅ Unique nonce per request
  • 'strict-dynamic' allows nonce-verified scripts to load other scripts
  • ✅ No 'self', 'unsafe-inline', or URL whitelists (ignored by 'strict-dynamic')
  • ✅ No inline scripts without nonce

Development:

script-src 'nonce-XXX' 'strict-dynamic' 'unsafe-eval'
script-src-elem 'nonce-XXX' 'strict-dynamic'
  • Adds 'unsafe-eval' to script-src only (for source maps and dev tools)
  • 'unsafe-eval' NOT added to script-src-elem (causes browser warning)

Styles: Pragmatic Approach

style-src 'self' 'unsafe-inline'
style-src-elem 'self' 'unsafe-inline'
style-src-attr 'unsafe-inline'

Why 'unsafe-inline' for styles:

  • React hydration injects styles before nonce available
  • Vite HMR injects styles dynamically
  • CSS-in-JS libraries generate runtime styles
  • Tailwind and other frameworks inject dynamic styles
  • Trade-off: Styles cannot execute code (low XSS risk)

This is the industry-standard approach used by GitHub, Google, and other major sites.

CSP Level 3 Support

The package properly handles granular directives (-elem, -attr):

  1. User rules can target base directives (script-src, style-src)
  2. Sources are automatically copied to granular directives
  3. CSP Level 3 browsers check granular directives first
  4. Exception: 'unsafe-eval' is NOT copied from script-src to script-src-elem (prevents browser warning)

How it works:

// Base directives (user or default)
script-src 'nonce-XXX' 'strict-dynamic' 'unsafe-eval'  // (dev mode)

// Automatically copied to granular directive (minus unsafe-eval)
script-src-elem 'nonce-XXX' 'strict-dynamic'  // No unsafe-eval here

// Result: Zero browser warnings

Examples

Multiple Service Rules

import { createCspMiddleware } from '@enalmada/start-secure';

const middleware = createCspMiddleware({
  rules: [
    {
      description: 'google-auth',
      'form-action': "'self' https://accounts.google.com",
      'img-src': "https://*.googleusercontent.com",
      'connect-src': "https://*.googleusercontent.com",
    },
    {
      description: 'sentry-monitoring',
      'worker-src': "blob:",
      'connect-src': "https://*.ingest.sentry.io",
    },
    {
      description: 'posthog-analytics',
      'script-src': "https://*.posthog.com",
      'connect-src': "https://*.posthog.com",
    },
  ],
  options: {
    isDev: process.env.NODE_ENV !== 'production',
  },
});

Custom Nonce Generator

import { createCspMiddleware } from '@enalmada/start-secure';

const middleware = createCspMiddleware({
  rules: [...],
  nonceGenerator: () => {
    // Custom nonce generation logic
    return customCryptoFunction();
  },
});

Additional Headers

import { createCspMiddleware } from '@enalmada/start-secure';

const middleware = createCspMiddleware({
  rules: [...],
  additionalHeaders: {
    'X-Custom-Header': 'value',
    'X-Powered-By': 'My App',
  },
});

Default Security Headers

The middleware automatically sets these security headers:

Content-Security-Policy: (built from rules + nonce)
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload (production only)
Permissions-Policy: camera=(), microphone=(), geolocation=(), ...

Migration from Handler Wrapper Pattern

If you're using the old createSecureHandler API, here's how to migrate:

Before (Handler Wrapper - Deprecated)

// src/server.ts
import { createSecureHandler } from '@enalmada/start-secure';

const secureHandler = createSecureHandler({
  rules: cspRules,
  options: { isDev: process.env.NODE_ENV !== 'production' }
});

export default {
  fetch: secureHandler(createStartHandler(defaultStreamHandler))
};

After (Middleware Pattern - Recommended)

// src/start.ts (NEW FILE)
import { createStart } from '@tanstack/react-start';
import { createCspMiddleware } from '@enalmada/start-secure';

export const startInstance = createStart(() => ({
  requestMiddleware: [
    createCspMiddleware({ rules: cspRules, options: { isDev: process.env.NODE_ENV !== 'production' } })
  ]
}));

// src/router.tsx (UPDATED)
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);

Benefits of Middleware Pattern

  • ✅ Per-request nonce generation (not static)
  • ✅ No 'unsafe-inline' for scripts in production
  • ✅ Integrates with TanStack router nonce support
  • ✅ Automatic nonce in all framework scripts
  • ✅ Cleaner, more maintainable code

Legacy API (Handler Wrapper)

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)

import { createSecureHandler } from '@enalmada/start-secure';

const secureHandler = createSecureHandler({
  rules: [
    { 'connect-src': 'https://api.example.com' }
  ],
  options: {
    isDev: process.env.NODE_ENV !== 'production'
  }
});

export default {
  fetch: secureHandler(createStartHandler(defaultStreamHandler))
};

Limitations:

  • ❌ Headers generated once at startup (no per-request nonces)
  • ❌ Falls back to 'unsafe-inline' for scripts
  • ❌ Doesn't integrate with TanStack router

Contributing

Contributions are welcome! Please open an issue or PR.

License

MIT © Adam Lane

Credits

Inspired by @enalmada/next-secure.