@pronghorn/cookie
v0.1.1
Published
Cookie parsing and signed-cookie middleware for Pronghorn. Reads incoming cookies into context.locals.cookies and provides setCookie/clearCookie helpers with HMAC signing support.
Downloads
42
Maintainers
Readme
Cookie 🍪
Cookie is a lightweight, TypeScript-first cookie parsing and signing middleware built as an external plugin for Pronghorn. It reads incoming Cookie headers into context.locals.cookies and exposes setCookie/clearCookie helpers that queue outgoing Set-Cookie headers, with optional HMAC signing to prevent client-side tampering.
Built as a standalone package (
@pronghorn/cookie), pairs naturally withcreateJwtAuthfor session-style, stateful auth flows alongside stateless bearer tokens.
Why Cookie
Pronghorn ships JWT auth for stateless bearer tokens, but has no built-in way to read or write cookies, which blocks session IDs, CSRF tokens, and "remember me" flows. Cookie fills that gap without pulling in a heavyweight cookie-jar dependency.
- Parses the
Cookierequest header into a flatRecord<string, string>oncontext.locals.cookies. setCookie/clearCookiehelpers queueSet-Cookieheaders, applied to the response after your handler runs.- Optional HMAC-SHA256 signing via
node:crypto, verified with a timing-safe comparison to prevent tampering and timing attacks. - Zero runtime dependencies beyond Node's built-in
cryptomodule (available in Bun). - Single global middleware, no plugin/decorator indirection needed since cookies are inherently per-request.
Installation
bun add @pronghorn/cookieRequires Bun >=1.3.0 and pronghorn >=0.1.2 as a peer dependency (used for typing the middleware only).
Quick Start
import { createApp } from 'pronghorn'
import { cookieParser } from '@pronghorn/cookie'
const app = createApp()
app.use(cookieParser({ secret: process.env.COOKIE_SECRET ?? 'dev-secret' }))
app.get('/', context => {
const visits = Number(context.locals.cookies['visits'] ?? '0') + 1
context.locals.setCookie('visits', String(visits), { signed: true, httpOnly: true, maxAge: 86_400 })
return context.json({ visits })
})
await app.listen(4000)Core Concepts
Reading cookies
Once cookieParser is registered globally, every request gets context.locals.cookies, a flat map of cookie name to decoded value. If a secret was provided and a cookie was signed, its value is automatically unsigned and verified before it reaches your handler.
app.get('/profile', context => {
const sessionId = context.locals.cookies['session']
if (!sessionId) return context.json({ error: 'No session' }, 401)
return context.json({ sessionId })
})Writing cookies
context.locals.setCookie(name, value, options?) queues a Set-Cookie header. Multiple calls in the same request each append their own header, matching how browsers expect multiple cookies to be set.
app.post('/login', context => {
context.locals.setCookie('session', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: 3600
})
return context.json({ loggedIn: true })
})Clearing cookies
context.locals.clearCookie(name, options?) sets the cookie to an empty value with Max-Age=0, causing the browser to delete it immediately.
app.post('/logout', context => {
context.locals.clearCookie('session')
return context.json({ loggedOut: true })
})Signed cookies
Pass signed: true when writing a cookie to append an HMAC-SHA256 signature, and provide a secret to cookieParser so incoming values are verified and stripped back to their original form automatically. This prevents users from editing cookie values client-side without invalidating the signature.
app.use(cookieParser({ secret: process.env.COOKIE_SECRET! }))
app.post('/cart', context => {
context.locals.setCookie('cartTotal', '49.99', { signed: true })
return context.json({ ok: true })
})
app.get('/cart', context => {
// Already verified and unsigned by cookieParser, tampering returns the raw (invalid) value instead
const total = context.locals.cookies['cartTotal']
return context.json({ total })
})Calling setCookie with signed: true but no secret configured throws immediately, so misconfiguration fails loudly instead of silently shipping unsigned cookies.
Cookie Options
| Option | Type | Default | Description |
| ---------- | ----------------------------- | ------- | --------------------------------------------------------- |
| maxAge | number | - | Lifetime in seconds; sent as Max-Age |
| expires | Date | - | Absolute expiration; sent as Expires |
| path | string | '/' | Cookie path scope |
| domain | string | - | Cookie domain scope |
| secure | boolean | false | Only sent over HTTPS |
| httpOnly | boolean | false | Inaccessible to client-side JS |
| sameSite | 'Strict' \| 'Lax' \| 'None' | - | Cross-site request behavior |
| signed | boolean | false | HMAC-signs the value; requires secret on cookieParser |
These map directly to standard Set-Cookie attributes.
Middleware Options
| Option | Type | Default | Description |
| -------- | -------- | ----------- | -------------------------------------------------------------- |
| secret | string | undefined | Enables signing/verification for cookies marked signed: true |
API Reference
cookieParser(options?: CookieParserOptions): Middleware - global middleware factory, register via app.use(cookieParser(options)).
| Locals property | Type | Description |
| ------------------------------------------------- | ------------------------ | ---------------------------------------------------- |
| context.locals.cookies | Record<string, string> | Parsed (and auto-unsigned) incoming cookies |
| context.locals.setCookie(name, value, options?) | void | Queues an outgoing Set-Cookie header |
| context.locals.clearCookie(name, options?) | void | Queues a Set-Cookie header that deletes the cookie |
Lower-level building blocks are also exported for advanced use outside the middleware:
import { parseCookieHeader, serializeCookie, signValue, unsignValue } from '@pronghorn/cookie'
const cookies = parseCookieHeader(request.headers.get('cookie'))
const header = serializeCookie('theme', 'dark', { maxAge: 604_800 })
const signed = signValue('user-42', 'my-secret')
const original = unsignValue(signed, 'my-secret') // 'user-42', or null if tamperedArchitecture
Cookie is split into three small modules, each with a single responsibility.
| Module | Responsibility |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| parser.ts | Parses the Cookie request header, and serializes outgoing cookies into Set-Cookie header values |
| signer.ts | HMAC-SHA256 signing/verification using node:crypto, with timingSafeEqual to avoid timing side-channels |
| middleware.ts | Wires parsing/signing into context.locals, and rebuilds the response with appended Set-Cookie headers after the handler resolves |
Because a Response returned by Response.json() has immutable headers, the middleware constructs a new Response with a merged Headers object rather than mutating the original, the same pattern used by Pronghorn's built-in cors middleware.
Security Notes
- Always set
httpOnly: trueon cookies holding session identifiers or tokens, to block access from client-side JavaScript. - Always set
secure: truein production so cookies are never sent over plain HTTP. - Use
signed: truefor any cookie whose value influences server-side logic (cart totals, role flags, etc.), unsigned cookies can be freely edited by the client. - Prefer
sameSite: 'Lax'or'Strict'to reduce CSRF exposure unless you specifically need cross-site cookie delivery.
License
WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.
