Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
dist/
node_modules/
.tmp/

.claude/settings.local.json
.env
6 changes: 6 additions & 0 deletions packages/cookie/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This is the changelog for [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie). It follows [semantic versioning](https://semver.org/).

## Unreleased

- BREAKING CHANGE: Rename `cookie.isSigned` to `cookie.signed`
- Add `createCookie` function to create a new `Cookie` object
- `CookieOptions` now extends `CookieProperties` so all cookie properties may be set in the `Cookie` constructor

## v0.2.0 (2025-11-04)

- Update `@remix-run/headers` peer dep to v0.15.0
Expand Down
2 changes: 1 addition & 1 deletion packages/cookie/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { type CookieOptions, Cookie } from './lib/cookie.ts'
export { createCookie, type CookieOptions, Cookie } from './lib/cookie.ts'
48 changes: 45 additions & 3 deletions packages/cookie/src/lib/cookie.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function getCookieFromSetCookie(setCookie: string): string {
return header.name + '=' + header.value
}

describe('cookie', () => {
describe('Cookie', () => {
it('parses/serializes empty string values', async () => {
let cookie = new Cookie('my-cookie')
let setCookie = await cookie.serialize('')
Expand Down Expand Up @@ -111,11 +111,11 @@ describe('cookie', () => {
it('is not signed by default', async () => {
let cookie = new Cookie('my-cookie')

assert.equal(cookie.isSigned, false)
assert.equal(cookie.signed, false)

let cookie2 = new Cookie('my-cookie2', { secrets: undefined })

assert.equal(cookie2.isSigned, false)
assert.equal(cookie2.signed, false)
})

it('uses Path=/ by default', async () => {
Expand All @@ -131,4 +131,46 @@ describe('cookie', () => {
})
assert.ok(setCookie2.includes('Path=/about'))
})

it('uses SameSite=Lax by default', async () => {
let cookie = new Cookie('my-cookie')
let setCookie = await cookie.serialize('hello world')
assert.ok(setCookie.includes('SameSite=Lax'))
})

it('supports overriding cookie properties in the constructor', async () => {
let cookie = new Cookie('my-cookie', {
domain: 'remix.run',
path: '/about',
maxAge: 3600,
sameSite: 'None',
secure: true,
httpOnly: true,
})
let setCookie = await cookie.serialize('hello world')
assert.ok(setCookie.includes('Domain=remix.run'))
assert.ok(setCookie.includes('Path=/about'))
assert.ok(setCookie.includes('Max-Age=3600'))
assert.ok(setCookie.includes('SameSite=None'))
assert.ok(setCookie.includes('Secure'))
assert.ok(setCookie.includes('HttpOnly'))
})

it('supports overriding cookie properties in the serialize method', async () => {
let cookie = new Cookie('my-cookie')
let setCookie = await cookie.serialize('hello world', {
domain: 'remix.run',
path: '/about',
maxAge: 3600,
sameSite: 'None',
secure: true,
httpOnly: true,
})
assert.ok(setCookie.includes('Domain=remix.run'))
assert.ok(setCookie.includes('Path=/about'))
assert.ok(setCookie.includes('Max-Age=3600'))
assert.ok(setCookie.includes('SameSite=None'))
assert.ok(setCookie.includes('Secure'))
assert.ok(setCookie.includes('HttpOnly'))
})
})
42 changes: 33 additions & 9 deletions packages/cookie/src/lib/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import {

import { sign, unsign } from './crypto.ts'

export interface CookieOptions {
/**
* Creates a new `Cookie` object.
* @param name The name of the cookie
* @param options The options for the cookie
* @returns A new cookie instance
*/
export function createCookie(name: string, options?: CookieOptions): Cookie {
return new Cookie(name, options)
}

export interface CookieOptions extends CookieProperties {
/**
* A function that decodes the cookie value.
*
Expand Down Expand Up @@ -41,22 +51,35 @@ export interface CookieOptions {
* to sign/unsign the value of the cookie to ensure it's not tampered with.
*/
export class Cookie {
constructor(name: string, options?: CookieOptions) {
let {
decode = decodeURIComponent,
encode = encodeURIComponent,
secrets = [],
...props
} = options ?? {}

this.name = name
this.#decode = decode
this.#encode = encode
this.#secrets = secrets
this.#props = props
}

/**
* The name of the cookie.
*/
readonly name: string

readonly #decode: (value: string) => string
readonly #encode: (value: string) => string
readonly #secrets: string[]

constructor(name: string, options?: CookieOptions) {
this.name = name
this.#decode = options?.decode ?? decodeURIComponent
this.#encode = options?.encode ?? encodeURIComponent
this.#secrets = options?.secrets ?? []
}
readonly #props: CookieProperties

/**
* True if this cookie uses one or more secrets for verification.
*/
get isSigned(): boolean {
get signed(): boolean {
return this.#secrets.length > 0
}

Expand Down Expand Up @@ -91,6 +114,7 @@ export class Cookie {
// sane defaults
path: '/',
sameSite: 'Lax',
...this.#props,
...props,
})

Expand Down
11 changes: 10 additions & 1 deletion packages/fetch-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"./logger-middleware": "./src/logger-middleware.ts",
"./method-override-middleware": "./src/method-override-middleware.ts",
"./response-helpers": "./src/response-helpers.ts",
"./session-middleware": "./src/session-middleware.ts",
"./package.json": "./package.json"
},
"publishConfig": {
Expand Down Expand Up @@ -54,18 +55,26 @@
"types": "./dist/response-helpers.d.ts",
"default": "./dist/response-helpers.js"
},
"./session-middleware": {
"types": "./dist/session-middleware.d.ts",
"default": "./dist/session-middleware.js"
},
"./package.json": "./package.json"
}
},
"devDependencies": {
"@remix-run/cookie": "workspace:*",
"@remix-run/session": "workspace:*",
"@types/node": "^24.6.0",
"typescript": "^5.9.3"
},
"peerDependencies": {
"@remix-run/cookie": "workspace:^",
"@remix-run/form-data-parser": "workspace:^",
"@remix-run/headers": "workspace:^",
"@remix-run/html-template": "workspace:^",
"@remix-run/route-pattern": "workspace:^"
"@remix-run/route-pattern": "workspace:^",
"@remix-run/session": "workspace:^"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
Expand Down
Loading