Skip to content

Commit 8be6b0f

Browse files
Bring over RR cookies code into @remix-run/cookie package (#10796)
Co-authored-by: Michael Jackson <[email protected]>
1 parent c2f7dd0 commit 8be6b0f

File tree

15 files changed

+753
-17
lines changed

15 files changed

+753
-17
lines changed

packages/cookie/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# `@remix-run/cookie` CHANGELOG
2+
3+
This is the changelog for [`@remix-run/cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie). It follows [semantic versioning](https://semver.org/).
4+
5+
## Unreleased

packages/cookie/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Michael Jackson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/cookie/README.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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)

packages/cookie/package.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@remix-run/cookie",
3+
"version": "0.1.0",
4+
"description": "A toolkit for working with cookies in JavaScript",
5+
"author": "Michael Jackson <[email protected]>",
6+
"license": "MIT",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/remix-run/remix.git",
10+
"directory": "packages/cookie"
11+
},
12+
"homepage": "https://github.com/remix-run/remix/tree/main/packages/cookie#readme",
13+
"files": [
14+
"LICENSE",
15+
"README.md",
16+
"dist",
17+
"src",
18+
"!src/**/*.test.ts"
19+
],
20+
"type": "module",
21+
"exports": {
22+
".": "./src/index.ts",
23+
"./package.json": "./package.json"
24+
},
25+
"publishConfig": {
26+
"exports": {
27+
".": {
28+
"types": "./dist/index.d.ts",
29+
"default": "./dist/index.js"
30+
},
31+
"./package.json": "./package.json"
32+
}
33+
},
34+
"devDependencies": {
35+
"@types/node": "^24.6.0"
36+
},
37+
"peerDependencies": {
38+
"@remix-run/headers": "workspace:*"
39+
},
40+
"scripts": {
41+
"build": "tsc -p tsconfig.build.json",
42+
"clean": "git clean -fdX",
43+
"prepublishOnly": "pnpm run build",
44+
"test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
45+
"typecheck": "tsc --noEmit"
46+
},
47+
"keywords": [
48+
"http",
49+
"cookie",
50+
"cookies",
51+
"http-cookies",
52+
"set-cookie"
53+
]
54+
}

packages/cookie/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { type CookieOptions, Cookie } from './lib/cookie.ts'

0 commit comments

Comments
 (0)