@nextrush/cookies
v3.0.5
Published
Cookie parsing and serialization middleware for NextRush
Downloads
429
Maintainers
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:
- Provide no security — leaving developers to manually handle encoding, validation, and signing
- Hide too much — auto-signing everything with no way to understand or customize
- 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/cookiesQuick 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:
- Parsing: When a request arrives, cookies are parsed from the
Cookieheader and sanitized - Reading:
ctx.state.cookies.get()retrieves parsed values - Writing:
ctx.state.cookies.set()queues cookies for the response - Serialization: After your handler runs, queued cookies are serialized to
Set-Cookieheaders
Signed cookies add a tamper-detection layer:
Original: session=abc123
Signed: session=abc123.HmacSignatureIf 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 viadocument.cookiesameSite: 'lax'— prevents CSRF on cross-origin POST requestspath: '/'— 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:
- New cookies are signed with
secret - Verification tries
secretfirst, thenpreviousSecretsin order - 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 invalidunsignCookieWithRotation(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: 86400sessionOptions(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 cookiecreateHostPrefixCookie(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 HTTPForgetting 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
