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

@nextrush/csrf

v1.0.0

Published

CSRF protection middleware for NextRush — Signed Double-Submit Cookie pattern with HMAC-SHA256

Readme

@nextrush/csrf

CSRF protection for NextRush apps using the signed double-submit cookie pattern — origin-checked, session-bound by explicit decision, and safe by default.

npm version downloads bundle size types ESM only license

| | | | --- | --- | | Purpose | Cross-site request forgery protection for state-changing routes | | Package type | Middleware | | Status | Beta 🚧 | | Included in nextrush? | ❌ No — standalone install | | Support tier | Public — stable (sealed public API) — see ADR-0005 | | Maintenance | Active | | Runtime | Universal — Node · Bun · Deno · Edge | | Requires | Node >=22 · ESM-only · TypeScript >=5.x | | Introduced | v1.0.0-beta.0 |

Highlights

  • Zero runtime dependencies — only a type-only dependency on @nextrush/types
  • ESM-only, tree-shakable, side-effect-free
  • Fully typed — strict TypeScript, zero any
  • 🔒 Secure by construction — origin checking on by default, session binding is an explicit decision, no query-string token fallback

The problem · When to use · Installation · Quick start · Capabilities · Mental model · Common tasks · API overview · Options · Compatibility · Troubleshooting · FAQ · Package relationships · Architecture · Resources


The problem

A browser sends cookies on cross-site requests automatically. If a state-changing route trusts a session cookie alone, any page on the web can trigger that route on a logged-in user's behalf — a form or fetch on an attacker's site rides the victim's authenticated session. The fix — a token the attacker's origin cannot read or forge — is easy to get wrong in exactly the ways that quietly disable it:

// TODAY, without this package — the thing that's easy to get wrong:
// a cookie with no Max-Age looks intentional but silently becomes a
// session cookie; an origin check that falls back to comparing
// against Host trusts a header the client fully controls; a token
// accepted from ?_csrf= leaks into logs, Referer headers, and history.

When to use

Use @nextrush/csrf if:

  • ✓ Your app serves HTML forms, or a SPA that talks to the same origin's API with cookie-based auth
  • ✓ You need origin validation and session-bound tokens without hand-rolling the double-submit pattern
  • ✓ You want the safe defaults (origin checking on, no query-string fallback) rather than opt-in hardening

Reach for something else if:

  • ✗ Every request already carries a bearer token in an Authorization header with no cookie involved → CSRF does not apply; skip this package
  • ✗ You need to sign a generic value (not a CSRF token) → use @nextrush/cookies' signedCookies(), which this package's token construction is modeled on

Installation

pnpm add @nextrush/csrf
# npm i @nextrush/csrf · yarn add @nextrush/csrf · bun add @nextrush/csrf

[!NOTE] Not included in the nextrush meta package — install separately.

Quick start

import { createApp, listen } from 'nextrush';
import { csrf } from '@nextrush/csrf';

const app = createApp();

const { protect, tokenProvider } = csrf({
  secret: process.env.CSRF_SECRET!,
  getSessionIdentifier: (ctx) => ctx.state.sessionId,
  allowedOrigins: ['https://example.com'],
});

app.use(protect);

app.get('/csrf-token', tokenProvider, async (ctx) => {
  ctx.json({ token: await ctx.state.csrf.generateToken() });
});

app.post('/api/transfer', (ctx) => {
  // reached only if the token, origin, and session all check out
  ctx.json({ ok: true });
});

listen(app, 8080);

protect runs on every request: it lets GET/HEAD/OPTIONS/TRACE through unconditionally, and validates the token, origin, and session binding on everything else. tokenProvider attaches the same ctx.state.csrf.generateToken() helper without enforcing protection, for the route that hands a fresh token to the client.

Capabilities

Capabilities

  • Signed double-submit cookie — an HMAC-SHA256 token in a non-httpOnly cookie, echoed back via header or body field
  • Origin validation — the Origin header checked against an explicit allowlist, never against Host; Sec-Fetch-Site: cross-site rejects before any cryptographic work
  • Session binding — tokens bound to a session identifier by default; the weaker unbound mode requires an explicit sessionBinding: 'none'
  • Path exclusionexcludePaths with /* (one segment) and /** (any depth) wildcard semantics, for endpoints authenticated another way (e.g. signed webhooks)

Developer experience

  • Fully typed, zero any
  • Fails at construction, not at request time, for missing secrets, missing session decisions, and missing origin allowlists
  • Tree-shakableconstantTimeEqual/generateToken/validateToken are also exported for advanced, direct use

Mental model

A token issued on a safe method's response cookie must come back, unmodified, on the next unsafe request — via a header or body field the attacker's origin cannot set on the victim's browser.

GET  ──▶ protect/tokenProvider ──▶ Set-Cookie: __Host-csrf=<hmac>.<random>
                                          │
POST ──▶ protect ──▶ origin check ──▶ shape check ──▶ constant-time compare ──▶ HMAC verify ──▶ next()
                          │                │
                    reject: 403      reject: 403 (zero crypto cost)

Rule: the cookie proves the browser saw the token; the header/body echo proves the caller could read it — an attacker's cross-origin page can trigger the cookie but never read its value to echo it back.

[!TIP] The full validation pipeline and token construction (Mermaid) are in ARCHITECTURE.md.


Common tasks

Issue a token to a server-rendered form

app.get('/checkout', tokenProvider, async (ctx) => {
  const token = await ctx.state.csrf.generateToken();
  ctx.html(`<form method="POST" action="/checkout">
    <input type="hidden" name="_csrf" value="${token}">
    ...
  </form>`);
});

Issue a token to a SPA and send it back via header

// server
app.get('/api/csrf-token', tokenProvider, async (ctx) => {
  ctx.json({ token: await ctx.state.csrf.generateToken() });
});

// client
const { token } = await fetch('/api/csrf-token').then((r) => r.json());
await fetch('/api/transfer', {
  method: 'POST',
  headers: { 'x-csrf-token': token },
  body: JSON.stringify({ amount: 100 }),
});

Exclude a webhook endpoint authenticated another way

csrf({
  secret: process.env.CSRF_SECRET!,
  sessionBinding: 'none',
  allowedOrigins: ['https://example.com'],
  excludePaths: ['/api/webhooks/*'], // exactly one segment past the prefix
});

Opt out of session binding intentionally

csrf({
  secret: process.env.CSRF_SECRET!,
  sessionBinding: 'none', // explicit acknowledgement — never the silent default
  allowedOrigins: ['https://example.com'],
});

API overview

| Export | Signature | Since | Stability | Description | | ------ | --------- | ----- | --------- | ----------- | | csrf | (options: CsrfOptions) => CsrfMiddleware | 1.0.0-beta.0 | Stable ✅ | Creates the protect and tokenProvider middleware pair | | generateToken | (secret, sessionId?, tokenSize?) => Promise<string> | 1.0.0-beta.0 | Stable ✅ | Mints an HMAC-signed token directly, bypassing the middleware | | validateToken | (token, secret, sessionId?) => Promise<boolean> | 1.0.0-beta.0 | Stable ✅ | Verifies a token's HMAC signature directly | | constantTimeEqual | (a: string, b: string) => Promise<boolean> | 1.0.0-beta.0 | Stable ✅ | Timing-safe string comparison, blinded with a per-process random key | | type CsrfOptions | — | 1.0.0-beta.0 | Stable ✅ | The csrf() configuration surface | | type CsrfMiddleware | — | 1.0.0-beta.0 | Stable ✅ | The { protect, tokenProvider } return shape | | type CsrfContext | — | 1.0.0-beta.0 | Stable ✅ | The ctx.state.csrf shape (generateToken, cookieToken) |

Options

| Option | Type | Required | Default | Security-sensitive | Description | | ------ | ---- | -------- | ------- | ------------------ | ----------- | | secret | string \| (() => string) | Yes | — | ⚠️ | HMAC key, ≥32 characters; a function enables key rotation | | getSessionIdentifier | (ctx) => string \| undefined | One of this or sessionBinding | — | ⚠️ | Binds tokens to a session; omission without sessionBinding: 'none' throws at construction | | sessionBinding | 'none' | One of this or getSessionIdentifier | — | ⚠️ | Explicit opt-out of session binding | | originCheck | boolean | No | true | ⚠️ | Validates Origin against allowedOrigins; never falls back to Host | | allowedOrigins | string[] | Required whenever originCheck is active | [] | ⚠️ | The only basis for origin validation | | cookie.maxAge | number | No | (omitted — session cookie) | — | Emitted verbatim only when set; negative/NaN/Infinity throw at construction | | cookie.name | string | No | '__Host-csrf' | ⚠️ | __Host- prefix enforces secure: true, no Domain, path: '/' | | cookie.sameSite | 'strict' \| 'lax' \| 'none' | No | 'strict' | ⚠️ | 'none' requires secure: true | | excludePaths | string[] | No | [] | ⚠️ | /* matches one remaining segment, /** any depth | | getTokenFromRequest | TokenExtractor | No | header/body extractor, no query string | ⚠️ | Custom extractors may opt back into query-string reads | | ignoredMethods | string[] | No | ['GET','HEAD','OPTIONS','TRACE'] | — | Methods exempt from validation | | tokenSize | number | No | 32 | — | Random-value size in bytes | | onError | (ctx, reason) => void \| Promise<void> | No | 403 JSON response | — | Custom validation-failure handler |

Compatibility

Requirements

| Requirement | Version | | ----------- | ------- | | NextRush | 3.x | | Node.js | >=22 | | TypeScript | >=5.x |

Runtimes

| Runtime | Supported | Notes | | ------- | --------- | ----- | | Node.js >=22 | ✅ | ESM-only | | Bun / Deno / Edge | ✅ / ✅ / ✅ | Web Crypto API only — no node:* imports |

Integration

  • Peer dependencies: none — a type-only dependency on @nextrush/types
  • Works with: @nextrush/cookies (independent cookie; no shared state), any session middleware supplying getSessionIdentifier
  • Incompatible with: none

[!IMPORTANT] NextRush is ESM-only, permanently — no CommonJS build. On Node >=22, CJS consumers can require() this ESM package natively. See the Module Format Policy.


Troubleshooting

Cause: originCheck defaults to true, and origin validation never falls back to Host — there is no other basis for the check. Fix: supply allowedOrigins, or set originCheck: false if the route is protected another way.

csrf({ secret, sessionBinding: 'none', allowedOrigins: ['https://example.com'] });

Cause: session binding is an intentional decision, never a silent default. Fix: supply getSessionIdentifier, or acknowledge the weaker mode explicitly.

csrf({ secret, sessionBinding: 'none', allowedOrigins: [...] });

Cause: the submitted token and the cookie token differ, or one was minted under a different sessionId. Fix: confirm the client echoes the exact cookie value via header/body, and that getSessionIdentifier returns the same value at issue and validation time.

FAQ

Why is the cookie not httpOnly? The double-submit pattern requires the client to read the cookie value and echo it back via header or body — an httpOnly cookie the client cannot read defeats the pattern.

Why ESM-only? See the Module Format Policy.

Does it work on Bun / Deno / Edge? Yes — token generation and verification use only the Web Crypto API (crypto.subtle, crypto.getRandomValues), with no Node-specific imports.

Why is the query string never read by default? A token in a URL leaks into access logs, Referer headers, and browser history. Pass a custom getTokenFromRequest to opt back in if a specific route requires it.


Package relationships

                 depends on            @nextrush/types
@nextrush/csrf ──────────────▶
                 often used with       @nextrush/cookies
                 usually used next     an application session middleware
  • Depends on: @nextrush/typesContext/Middleware types only
  • Often used with: @nextrush/cookies — for the application's own session cookie; this package manages its CSRF cookie independently
  • Usually used next: an application-supplied getSessionIdentifier, once a session/auth layer exists
  • Alternative: none — CSRF protection has no in-repo substitute; skip this package entirely for bearer-token-only APIs with no cookie-based auth

Architecture

Maintaining or contributing to this package? The internal design — token construction, the validation pipeline, invariants, decisions and trade-offs (with diagrams) — is in ARCHITECTURE.md.

Resources


MIT © Tanzim Hossain