|
| 1 | +# @remix-run/cookie |
| 2 | + |
| 3 | +Simplify HTTP cookie management in JavaScript with type-safe, secure cookie handling. `@remix-run/cookie` provides a clean, intuitive API for creating, parsing, and serializing HTTP cookies with built-in support for signing, secret rotation, and comprehensive cookie attribute management. |
| 4 | + |
| 5 | +HTTP cookies are essential for web applications—from session management and user preferences to authentication tokens and tracking. While the standard cookie parsing libraries provide basic functionality, they often leave complex scenarios like secure signing, secret rotation, and type-safe value handling up to you. |
| 6 | + |
| 7 | +`@remix-run/cookie` solves this by offering: |
| 8 | + |
| 9 | +- **Secure Cookie Signing:** Built-in cryptographic signing using HMAC-SHA256 to prevent cookie tampering, with support for secret rotation without breaking existing cookies. |
| 10 | +- **Type-Safe Value Handling:** Automatically serializes and deserializes JavaScript values (strings, objects, booleans, numbers) to/from cookie-safe formats. |
| 11 | +- **Comprehensive Cookie Attributes:** Full support for all standard cookie attributes including `Path`, `Domain`, `Secure`, `HttpOnly`, `SameSite`, `Max-Age`, and `Expires`. |
| 12 | +- **Reusable Cookie Containers:** Create logical cookie containers that can be used to parse and serialize multiple values over time. |
| 13 | +- **Web Standards Compliant:** Built on Web Crypto API and standard cookie parsing, making it runtime-agnostic (Node.js, Bun, Deno, Cloudflare Workers). |
| 14 | +- **Secret Rotation Support:** Seamlessly rotate signing secrets while maintaining backward compatibility with existing cookies. |
| 15 | + |
| 16 | +Perfect for building secure, maintainable cookie management in your JavaScript and TypeScript applications! |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +```sh |
| 21 | +npm install @remix-run/cookie |
| 22 | +``` |
| 23 | + |
| 24 | +## Overview |
| 25 | + |
| 26 | +The following should give you a sense of what kinds of things you can do with this library: |
| 27 | + |
| 28 | +```ts |
| 29 | +import { Cookie } from '@remix-run/cookie' |
| 30 | + |
| 31 | +// Create a basic cookie |
| 32 | +let sessionCookie = new Cookie('session') |
| 33 | + |
| 34 | +// Serialize a value to a Set-Cookie header |
| 35 | +let setCookieHeader = await sessionCookie.serialize({ |
| 36 | + userId: '12345', |
| 37 | + theme: 'dark', |
| 38 | +}) |
| 39 | +console.log(setCookieHeader) |
| 40 | +// session=eyJ1c2VySWQiOiIxMjM0NSIsInRoZW1lIjoiZGFyayJ9; Path=/; SameSite=Lax |
| 41 | + |
| 42 | +// Parse a Cookie header to get the value back |
| 43 | +let cookieHeader = 'session=eyJ1c2VySWQiOiIxMjM0NSIsInRoZW1lIjoiZGFyayJ9' |
| 44 | +let sessionData = await sessionCookie.parse(cookieHeader) |
| 45 | +console.log(sessionData) // { userId: '12345', theme: 'dark' } |
| 46 | + |
| 47 | +// Create a signed cookie for security |
| 48 | +let secureCookie = new Cookie('secure-session', { |
| 49 | + secrets: ['Secr3t'], // Array to support secret rotation |
| 50 | + httpOnly: true, |
| 51 | + secure: true, |
| 52 | + sameSite: 'strict', |
| 53 | + maxAge: 60 * 60 * 24 * 7, // 7 days |
| 54 | +}) |
| 55 | + |
| 56 | +// Signed cookies prevent tampering |
| 57 | +let signedValue = await secureCookie.serialize({ admin: true }) |
| 58 | +console.log(signedValue) |
| 59 | +// secure-session=eyJhZG1pbiI6dHJ1ZX0.signature; Path=/; Max-Age=604800; HttpOnly; Secure; SameSite=Strict |
| 60 | + |
| 61 | +let parsedValue = await secureCookie.parse('secure-session=eyJhZG1pbiI6dHJ1ZX0.signature') |
| 62 | +console.log(parsedValue) // { admin: true } |
| 63 | + |
| 64 | +// Tampered cookies return null |
| 65 | +let tamperedValue = await secureCookie.parse('secure-session=eyJhZG1pbiI6ZmFsc2V9.badsignature') |
| 66 | +console.log(tamperedValue) // null |
| 67 | + |
| 68 | +// Cookie properties |
| 69 | +console.log(secureCookie.name) // 'secure-session' |
| 70 | +console.log(secureCookie.isSigned) // true |
| 71 | +console.log(secureCookie.expires) // Date object (calculated from maxAge) |
| 72 | + |
| 73 | +// Handle different data types |
| 74 | +let preferencesCookie = new Cookie('preferences') |
| 75 | + |
| 76 | +// Strings |
| 77 | +await preferencesCookie.serialize('light-mode') |
| 78 | + |
| 79 | +// Objects |
| 80 | +await preferencesCookie.serialize({ |
| 81 | + theme: 'dark', |
| 82 | + language: 'en-US', |
| 83 | + notifications: true, |
| 84 | +}) |
| 85 | + |
| 86 | +// Booleans |
| 87 | +await preferencesCookie.serialize(false) |
| 88 | + |
| 89 | +// Numbers |
| 90 | +await preferencesCookie.serialize(42) |
| 91 | +``` |
| 92 | + |
| 93 | +## Cookie Configuration |
| 94 | + |
| 95 | +Cookies can be configured with comprehensive options: |
| 96 | + |
| 97 | +```ts |
| 98 | +import { Cookie } from '@remix-run/cookie' |
| 99 | + |
| 100 | +let cookie = new Cookie('my-cookie', { |
| 101 | + // Security options |
| 102 | + secrets: ['secret1', 'secret2'], // For signing (first used for new cookies) |
| 103 | + httpOnly: true, // Prevent JavaScript access |
| 104 | + secure: true, // Require HTTPS |
| 105 | + |
| 106 | + // Scope options |
| 107 | + domain: '.example.com', // Cookie domain |
| 108 | + path: '/admin', // Cookie path |
| 109 | + |
| 110 | + // Expiration options |
| 111 | + maxAge: 60 * 60 * 24, // Max age in seconds |
| 112 | + expires: new Date('2025-12-31'), // Absolute expiration date |
| 113 | + |
| 114 | + // SameSite options |
| 115 | + sameSite: 'strict', // 'strict' | 'lax' | 'none' |
| 116 | + |
| 117 | + // Encoding options (from 'cookie' package) |
| 118 | + encode: (value) => encodeURIComponent(value), |
| 119 | + decode: (value) => decodeURIComponent(value), |
| 120 | +}) |
| 121 | +``` |
| 122 | + |
| 123 | +## Secret Rotation |
| 124 | + |
| 125 | +One of the key features is seamless secret rotation for signed cookies: |
| 126 | + |
| 127 | +```ts |
| 128 | +// Start with an initial secret |
| 129 | +let cookie = new Cookie('session', { |
| 130 | + secrets: ['secret-v1'], |
| 131 | +}) |
| 132 | + |
| 133 | +let setCookie1 = await cookie.serialize({ user: 'alice' }) |
| 134 | + |
| 135 | +// Later, rotate to a new secret while keeping the old one |
| 136 | +cookie = new Cookie('session', { |
| 137 | + secrets: ['secret-v2', 'secret-v1'], // New secret first, old ones after |
| 138 | +}) |
| 139 | + |
| 140 | +// New cookies use the new secret |
| 141 | +let setCookie2 = await cookie.serialize({ user: 'bob' }) |
| 142 | + |
| 143 | +// But old cookies still work |
| 144 | +let oldValue = await cookie.parse(setCookie1.split(';')[0]) |
| 145 | +console.log(oldValue) // { user: 'alice' } - still works! |
| 146 | + |
| 147 | +let newValue = await cookie.parse(setCookie2.split(';')[0]) |
| 148 | +console.log(newValue) // { user: 'bob' } |
| 149 | +``` |
| 150 | + |
| 151 | +## Advanced Usage |
| 152 | + |
| 153 | +### Custom Serialization Options |
| 154 | + |
| 155 | +You can override cookie options when serializing: |
| 156 | + |
| 157 | +```ts |
| 158 | +let cookie = new Cookie('flexible', { |
| 159 | + maxAge: 60 * 60, // Default 1 hour |
| 160 | +}) |
| 161 | + |
| 162 | +// Override for a specific use case |
| 163 | +let longLivedCookie = await cookie.serialize('remember-me', { |
| 164 | + maxAge: 60 * 60 * 24 * 365, // 1 year |
| 165 | +}) |
| 166 | + |
| 167 | +let sessionCookie = await cookie.serialize('temp-data', { |
| 168 | + maxAge: undefined, // Session cookie (no expiration) |
| 169 | + secure: false, // Maybe for development |
| 170 | +}) |
| 171 | +``` |
| 172 | + |
| 173 | +### Error Handling |
| 174 | + |
| 175 | +The library handles various error scenarios gracefully: |
| 176 | + |
| 177 | +```ts |
| 178 | +let cookie = new Cookie('test') |
| 179 | + |
| 180 | +// Missing or malformed cookie headers return null |
| 181 | +await cookie.parse(null) // null |
| 182 | +await cookie.parse('') // null |
| 183 | +await cookie.parse('other=value') // null |
| 184 | + |
| 185 | +// Malformed cookie values return empty object or null |
| 186 | +await cookie.parse('test=invalid-base64@#$') // {} |
| 187 | + |
| 188 | +// Signed cookies with bad signatures return null |
| 189 | +let signedCookie = new Cookie('signed', { secrets: ['secret'] }) |
| 190 | +await signedCookie.parse('signed=value.badsignature') // null |
| 191 | +``` |
| 192 | + |
| 193 | +## Related Packages |
| 194 | + |
| 195 | +- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Type-safe HTTP header manipulation |
| 196 | +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Build HTTP routers using the web fetch API |
| 197 | +- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - Build HTTP servers on Node.js using the web fetch API |
| 198 | + |
| 199 | +## License |
| 200 | + |
| 201 | +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) |
0 commit comments