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/cookies

v3.0.5

Published

Cookie parsing and serialization middleware for NextRush

Downloads

429

Readme

@nextrush/cookies

Secure, RFC 6265-compliant cookie middleware with HMAC signing, CRLF injection protection, and key rotation support.

The Problem

Cookies seem simple until they aren't. Session hijacking, CRLF injection, cross-site request forgery—these attacks exploit frameworks that treat cookies as plain strings. Most cookie libraries either:

  1. Provide no security — leaving developers to manually handle encoding, validation, and signing
  2. Hide too much — auto-signing everything with no way to understand or customize
  3. Lack key rotation — forcing service downtime when you need to rotate secrets

You need cookies that are secure by default, transparent in behavior, and flexible when requirements change.

What NextRush Does Differently

@nextrush/cookies provides production-grade cookie handling:

  • Security-first defaults — HttpOnly, SameSite=Lax, Path=/ applied automatically
  • CRLF injection prevention — Sanitizes values before serialization
  • Cookie prefix validation — Enforces __Secure- and __Host- requirements per spec
  • HMAC-SHA256 signing — Web Crypto API for runtime compatibility (Node.js, Bun, Edge)
  • Key rotation — Verify with current key, fall back to previous keys during rotation
  • Explicit API — No hidden magic, every behavior documented

Installation

pnpm add @nextrush/cookies

Quick Start

import { createApp } from '@nextrush/core';
import { cookies } from '@nextrush/cookies';

const app = createApp();

app.use(cookies());

app.get('/profile', async (ctx) => {
  const session = ctx.state.cookies.get('session');
  ctx.json({ session });
});

app.post('/login', async (ctx) => {
  ctx.state.cookies.set('session', 'user-123', {
    httpOnly: true,
    secure: true,
    maxAge: 86400, // 1 day in seconds
  });
  ctx.json({ success: true });
});

app.post('/logout', async (ctx) => {
  ctx.state.cookies.delete('session');
  ctx.json({ success: true });
});

Mental Model

Think of this middleware as a cookie vault:

  1. Parsing: When a request arrives, cookies are parsed from the Cookie header and sanitized
  2. Reading: ctx.state.cookies.get() retrieves parsed values
  3. Writing: ctx.state.cookies.set() queues cookies for the response
  4. Serialization: After your handler runs, queued cookies are serialized to Set-Cookie headers

Signed cookies add a tamper-detection layer:

Original: session=abc123
Signed:   session=abc123.HmacSignature

If someone modifies abc123, the signature won't match, and get() returns undefined.

Default Behavior

With zero configuration, cookies() applies these defaults to every cookie you set:

  • httpOnly: true — blocks JavaScript access via document.cookie
  • sameSite: 'lax' — prevents CSRF on cross-origin POST requests
  • path: '/' — scopes the cookie to the entire domain

Values are URL-decoded and sanitized (CRLF characters stripped) during parsing.

Signed Cookies

For tamper-proof cookies, use the signed cookies middleware:

import { signedCookies } from '@nextrush/cookies';

app.use(
  signedCookies({
    secret: process.env.COOKIE_SECRET!,
  })
);

app.post('/auth', async (ctx) => {
  await ctx.state.signedCookies.set('userId', 'user-456', {
    httpOnly: true,
    secure: true,
  });
  ctx.json({ success: true });
});

app.get('/profile', async (ctx) => {
  const userId = await ctx.state.signedCookies.get('userId');
  if (!userId) {
    ctx.status = 401;
    return ctx.json({ error: 'Invalid session' });
  }
  ctx.json({ userId });
});

Key Rotation

When rotating secrets, provide previous keys to maintain session continuity:

app.use(
  signedCookies({
    secret: process.env.COOKIE_SECRET_NEW!,
    previousSecrets: [process.env.COOKIE_SECRET_OLD!],
  })
);

During rotation:

  1. New cookies are signed with secret
  2. Verification tries secret first, then previousSecrets in order
  3. Old sessions remain valid until they naturally expire

Security Features

CRLF Injection Prevention

Cookie values are automatically sanitized:

// Attacker tries: "value\r\nSet-Cookie: evil=payload"
// Result: "valueSet-Cookie: evil=payload" (CRLF removed)
ctx.state.cookies.set('safe', 'value\r\nSet-Cookie: evil=payload');

Cookie Prefix Enforcement

The __Secure- and __Host- prefixes have strict requirements:

import { serializeCookie } from '@nextrush/cookies';

// Valid: __Secure- requires secure=true
serializeCookie('__Secure-token', 'value', { secure: true });

// Valid: __Host- requires secure=true, path='/', no domain
serializeCookie('__Host-session', 'value', { secure: true, path: '/' });

// Throws SecurityError: __Secure- without secure flag
serializeCookie('__Secure-token', 'value', { secure: false });

// Throws SecurityError: __Host- with domain
serializeCookie('__Host-session', 'value', { secure: true, domain: 'example.com' });

Public Suffix Blocking

Prevents setting cookies on TLDs:

// Throws SecurityError: Cannot set cookie on public suffix
serializeCookie('session', 'value', { domain: '.com' });
serializeCookie('session', 'value', { domain: '.co.uk' });

Size Limits

Cookies exceeding 4KB are rejected:

// Throws RangeError: Cookie exceeds maximum size
serializeCookie('huge', 'x'.repeat(5000));

API Reference

Middleware

cookies(options?)

Creates cookie middleware that adds ctx.state.cookies.

function cookies(options?: CookieMiddlewareOptions): Middleware;

Options:

| Option | Type | Default | Description | | -------- | --------------------------- | -------------------- | ---------------------------------------- | | decode | (value: string) => string | decodeURIComponent | Custom decode function for cookie values |

Context API (ctx.state.cookies):

| Method | Signature | Description | | -------- | --------------------------------------------------------------------------- | ---------------------- | | get | (name: string) => string \| undefined | Get cookie value | | set | (name: string, value: string, options?: CookieOptions) => void | Set cookie | | delete | (name: string, options?: Pick<CookieOptions, 'domain' \| 'path'>) => void | Delete cookie | | all | () => Record<string, string> | Get all cookies | | has | (name: string) => boolean | Check if cookie exists |

signedCookies(options)

Creates signed cookie middleware that adds ctx.state.signedCookies.

function signedCookies(options: SignedCookieMiddlewareOptions): Middleware;

Options:

| Option | Type | Required | Description | | ----------------- | ---------- | -------- | --------------------------------- | | secret | string | Yes | Current signing secret | | previousSecrets | string[] | No | Previous secrets for key rotation |

Context API (ctx.state.signedCookies):

| Method | Signature | Description | | -------- | --------------------------------------------------------------------------- | ---------------------------- | | get | (name: string) => Promise<string \| undefined> | Get and verify signed cookie | | set | (name: string, value: string, options?: CookieOptions) => Promise<void> | Set signed cookie | | delete | (name: string, options?: Pick<CookieOptions, 'domain' \| 'path'>) => void | Delete cookie |

Utility Functions

parseCookies(header, options?)

Parse a Cookie header string.

function parseCookies(
  header: string | null | undefined,
  options?: ParseOptions
): Record<string, string>;

ParseOptions:

| Option | Type | Default | Description | | ------------ | --------- | ------- | ------------------------------------- | | decode | boolean | true | URL-decode cookie values | | sanitize | boolean | true | Remove control characters from values | | maxCookies | number | 50 | Maximum number of cookies to parse |

parseCookies('name=value; session=abc123');
// { name: 'value', session: 'abc123' }

serializeCookie(name, value, options?)

Serialize a cookie for Set-Cookie header.

function serializeCookie(name: string, value: string, options?: CookieOptions): string;
serializeCookie('session', 'abc123', { httpOnly: true, secure: true });
// 'session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax'

signCookie(value, secret)

Sign a cookie value with HMAC-SHA256.

function signCookie(value: string, secret: string): Promise<string>;
await signCookie('user-123', 'secret');
// 'user-123.BASE64_SIGNATURE'

unsignCookie(signedValue, secret)

Verify and extract a signed cookie value.

function unsignCookie(signedValue: string, secret: string): Promise<string | undefined>;
await unsignCookie('user-123.BASE64_SIGNATURE', 'secret');
// 'user-123' or undefined if invalid

unsignCookieWithRotation(signedValue, keys)

Verify with key rotation support.

function unsignCookieWithRotation(
  signedValue: string,
  keys: SigningKeys
): Promise<string | undefined>;

SigningKeys:

| Property | Type | Required | Description | | ---------- | ---------- | -------- | --------------------------------------- | | current | string | Yes | Primary key for signing new cookies | | previous | string[] | No | Previous keys for verifying old cookies |

Helper Functions

secureOptions(options?)

Returns secure cookie preset. Merges your options with httpOnly: true, secure: true, sameSite: 'strict', path: '/'.

function secureOptions(options?: CookieOptions): CookieOptions;
ctx.state.cookies.set('session', value, secureOptions({ maxAge: 86400 }));
// httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 86400

sessionOptions(options?)

Returns session cookie preset. Merges your options with httpOnly: true, sameSite: 'lax', path: '/'. Sets maxAge and expires to undefined so the cookie expires when the browser closes.

function sessionOptions(options?: CookieOptions): CookieOptions;
ctx.state.cookies.set('temp', value, sessionOptions());
// httpOnly: true, sameSite: 'lax', path: '/'

createSecurePrefixCookie(name, value, options?)

Create a __Secure- prefixed cookie. Auto-sets secure: true.

createSecurePrefixCookie('token', 'value', { maxAge: 3600 });
// Validates prefix requirements, returns serialized cookie

createHostPrefixCookie(name, value, options?)

Create a __Host- prefixed cookie. Auto-sets secure: true, path: '/', and removes domain.

createHostPrefixCookie('session', 'value');
// Validates prefix requirements (secure=true, path='/', no domain)

Types

import type {
  CookieOptions,
  CookieContext,
  CookieState,
  CookieMiddlewareOptions,
  SignedCookieContext,
  SignedCookieState,
  SignedCookieMiddlewareOptions,
  ParseOptions,
  ParsedCookies,
  SameSiteValue,
  CookiePriority,
  SigningKeys,
} from '@nextrush/cookies';

CookieOptions

interface CookieOptions {
  domain?: string;
  expires?: Date | number;
  httpOnly?: boolean;
  maxAge?: number;
  path?: string;
  sameSite?: SameSiteValue;
  secure?: boolean;
  priority?: CookiePriority;
  partitioned?: boolean;
}

Common Mistakes

Setting secure cookies without HTTPS

// Won't work: secure cookies require HTTPS
ctx.state.cookies.set('session', 'value', { secure: true });
// Browser ignores cookie on HTTP

Forgetting to await signed cookie operations

// Wrong — returns Promise, not value
const userId = ctx.state.signedCookies.get('userId');

// Correct
const userId = await ctx.state.signedCookies.get('userId');

Using SameSite=None without Secure

// Browsers reject this combination
ctx.state.cookies.set('cross', 'value', { sameSite: 'none' });

// Correct
ctx.state.cookies.set('cross', 'value', { sameSite: 'none', secure: true });

When NOT to Use

  • Large data storage — Cookies have a 4KB limit; use sessions with server-side storage
  • Sensitive data without signing — Never store passwords or tokens in unsigned cookies
  • Cross-domain state — Consider tokens or other mechanisms for cross-origin authentication

Runtime Compatibility

This package uses the Web Crypto API and works in:

  • Node.js 20+
  • Bun
  • Cloudflare Workers
  • Deno
  • Vercel Edge Runtime

License

MIT