@remix-run/cookie 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/{crypto.d.ts → cookie-signing.d.ts} +1 -1
- package/dist/lib/cookie-signing.d.ts.map +1 -0
- package/dist/lib/{crypto.js → cookie-signing.js} +5 -2
- package/dist/lib/cookie.d.ts +86 -32
- package/dist/lib/cookie.d.ts.map +1 -1
- package/dist/lib/cookie.js +70 -69
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/lib/{crypto.ts → cookie-signing.ts} +8 -4
- package/src/lib/cookie.ts +166 -84
- package/dist/lib/crypto.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -8,8 +8,6 @@ HTTP cookies are essential for web applications, from session management and use
|
|
|
8
8
|
|
|
9
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
10
|
- **Secret Rotation Support:** Seamlessly rotate signing secrets while maintaining backward compatibility with existing cookies.
|
|
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
11
|
- **Web Standards Compliant:** Built on Web Crypto API and standard cookie parsing, making it runtime-agnostic (Node.js, Bun, Deno, Cloudflare Workers).
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
@@ -21,9 +19,18 @@ npm install @remix-run/cookie
|
|
|
21
19
|
## Usage
|
|
22
20
|
|
|
23
21
|
```tsx
|
|
24
|
-
import {
|
|
22
|
+
import { createCookie } from '@remix-run/cookie'
|
|
23
|
+
|
|
24
|
+
let sessionCookie = createCookie('session', {
|
|
25
|
+
httpOnly: true,
|
|
26
|
+
secrets: ['s3cret1'],
|
|
27
|
+
secure: true,
|
|
28
|
+
})
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
cookie.name // "session"
|
|
31
|
+
cookie.httpOnly // true
|
|
32
|
+
cookie.secure // true
|
|
33
|
+
cookie.signed // true
|
|
27
34
|
|
|
28
35
|
// Get the value of the "session" cookie from the request's `Cookie` header
|
|
29
36
|
let value = await sessionCookie.parse(request.headers.get('Cookie'))
|
|
@@ -46,7 +53,7 @@ Secret rotation is also supported, so you can easily rotate in new secrets witho
|
|
|
46
53
|
import { Cookie } from '@remix-run/cookie'
|
|
47
54
|
|
|
48
55
|
// Start with a single secret
|
|
49
|
-
let sessionCookie =
|
|
56
|
+
let sessionCookie = createCookie('session', {
|
|
50
57
|
secrets: ['secret1'],
|
|
51
58
|
})
|
|
52
59
|
|
|
@@ -62,12 +69,19 @@ let response = new Response('Hello, world!', {
|
|
|
62
69
|
All cookies sent in this scenario will be signed with the secret `secret1`. Later, when it's time to rotate secrets, add a new secret to the beginning of the array and all existing cookies will still be able to be parsed.
|
|
63
70
|
|
|
64
71
|
```tsx
|
|
65
|
-
let sessionCookie =
|
|
72
|
+
let sessionCookie = createCookie('session', {
|
|
66
73
|
secrets: ['secret2', 'secret1'],
|
|
67
74
|
})
|
|
68
75
|
|
|
69
|
-
// This
|
|
76
|
+
// This works for cookies signed with either secret
|
|
70
77
|
let value = await sessionCookie.parse(request.headers.get('Cookie'))
|
|
78
|
+
|
|
79
|
+
// Newly serialized cookies will be signed with the new secret
|
|
80
|
+
let response = new Response('Hello, world!', {
|
|
81
|
+
headers: {
|
|
82
|
+
'Set-Cookie': await sessionCookie.serialize(value),
|
|
83
|
+
},
|
|
84
|
+
})
|
|
71
85
|
```
|
|
72
86
|
|
|
73
87
|
### Custom Encoding
|
|
@@ -75,7 +89,7 @@ let value = await sessionCookie.parse(request.headers.get('Cookie'))
|
|
|
75
89
|
By default, the library will use `encodeURIComponent` and `decodeURIComponent` to encode and decode the cookie value. This is suitable for most use cases, but you can provide your own functions to customize the encoding and decoding of the cookie value.
|
|
76
90
|
|
|
77
91
|
```tsx
|
|
78
|
-
let sessionCookie =
|
|
92
|
+
let sessionCookie = createCookie('session', {
|
|
79
93
|
encode: (value) => value,
|
|
80
94
|
decode: (value) => value,
|
|
81
95
|
})
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { type Cookie, type CookieOptions, createCookie } from './lib/cookie.ts';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createCookie
|
|
1
|
+
export { createCookie } from "./lib/cookie.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-signing.d.ts","sourceRoot":"","sources":["../../src/lib/cookie-signing.ts"],"names":[],"mappings":"AAEA,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzE;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,CAuBpF"}
|
|
@@ -8,12 +8,15 @@ export async function sign(value, secret) {
|
|
|
8
8
|
}
|
|
9
9
|
export async function unsign(cookie, secret) {
|
|
10
10
|
let index = cookie.lastIndexOf('.');
|
|
11
|
+
if (index === -1) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
11
14
|
let value = cookie.slice(0, index);
|
|
12
15
|
let hash = cookie.slice(index + 1);
|
|
13
16
|
let data = encoder.encode(value);
|
|
14
17
|
let key = await createKey(secret, ['verify']);
|
|
15
18
|
try {
|
|
16
|
-
let signature =
|
|
19
|
+
let signature = byteStringToArray(atob(hash));
|
|
17
20
|
let valid = await crypto.subtle.verify('HMAC', key, signature, data);
|
|
18
21
|
return valid ? value : false;
|
|
19
22
|
}
|
|
@@ -27,7 +30,7 @@ export async function unsign(cookie, secret) {
|
|
|
27
30
|
async function createKey(secret, usages) {
|
|
28
31
|
return crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, usages);
|
|
29
32
|
}
|
|
30
|
-
function
|
|
33
|
+
function byteStringToArray(byteString) {
|
|
31
34
|
let array = new Uint8Array(byteString.length);
|
|
32
35
|
for (let i = 0; i < byteString.length; i++) {
|
|
33
36
|
array[i] = byteString.charCodeAt(i);
|
package/dist/lib/cookie.d.ts
CHANGED
|
@@ -1,11 +1,86 @@
|
|
|
1
1
|
import { type CookieProperties } from '@remix-run/headers';
|
|
2
|
+
type SameSiteValue = 'Strict' | 'Lax' | 'None';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Represents a HTTP cookie.
|
|
5
|
+
*
|
|
6
|
+
* Supports parsing and serializing the cookie to/from `Cookie` and `Set-Cookie` headers.
|
|
7
|
+
*
|
|
8
|
+
* Also supports cryptographic signing of the cookie value to ensure it's not tampered with, and
|
|
9
|
+
* secret rotation to easily rotate secrets without breaking existing cookies.
|
|
7
10
|
*/
|
|
8
|
-
export
|
|
11
|
+
export interface Cookie {
|
|
12
|
+
/**
|
|
13
|
+
* The domain of the cookie.
|
|
14
|
+
*
|
|
15
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#domaindomain-value)
|
|
16
|
+
*/
|
|
17
|
+
readonly domain: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The expiration date of the cookie.
|
|
20
|
+
*
|
|
21
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#expiresdate)
|
|
22
|
+
*/
|
|
23
|
+
readonly expires: Date | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* True if the cookie is HTTP-only.
|
|
26
|
+
*
|
|
27
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#httponly)
|
|
28
|
+
*/
|
|
29
|
+
readonly httpOnly: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The maximum age of the cookie in seconds.
|
|
32
|
+
*
|
|
33
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#max-agenumber)
|
|
34
|
+
*/
|
|
35
|
+
readonly maxAge: number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The name of the cookie.
|
|
38
|
+
*
|
|
39
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie-namecookie-value)
|
|
40
|
+
*/
|
|
41
|
+
readonly name: string;
|
|
42
|
+
/**
|
|
43
|
+
* Extracts the value of this cookie from a `Cookie` header value.
|
|
44
|
+
* @param headerValue The `Cookie` header to parse
|
|
45
|
+
* @returns The value of this cookie, or `null` if it's not present
|
|
46
|
+
*/
|
|
47
|
+
parse(headerValue: string | null): Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* True if the cookie is partitioned.
|
|
50
|
+
*
|
|
51
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#partitioned)
|
|
52
|
+
*/
|
|
53
|
+
readonly partitioned: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The path of the cookie. Defaults to `/`.
|
|
56
|
+
*
|
|
57
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value)
|
|
58
|
+
*/
|
|
59
|
+
readonly path: string;
|
|
60
|
+
/**
|
|
61
|
+
* The `SameSite` attribute of the cookie. Defaults to `Lax`.
|
|
62
|
+
*
|
|
63
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)
|
|
64
|
+
*/
|
|
65
|
+
readonly sameSite: SameSiteValue;
|
|
66
|
+
/**
|
|
67
|
+
* True if the cookie is secure (only sent over HTTPS).
|
|
68
|
+
*
|
|
69
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure)
|
|
70
|
+
*/
|
|
71
|
+
readonly secure: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the value to use in a `Set-Cookie` header for this cookie.
|
|
74
|
+
* @param value The value to serialize
|
|
75
|
+
* @param props (optional) Additional properties to use when serializing the cookie
|
|
76
|
+
* @returns The `Set-Cookie` header for this cookie
|
|
77
|
+
*/
|
|
78
|
+
serialize(value: string, props?: CookieProperties): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* True if this cookie uses one or more secrets for verification.
|
|
81
|
+
*/
|
|
82
|
+
readonly signed: boolean;
|
|
83
|
+
}
|
|
9
84
|
export interface CookieOptions extends CookieProperties {
|
|
10
85
|
/**
|
|
11
86
|
* A function that decodes the cookie value.
|
|
@@ -36,32 +111,11 @@ export interface CookieOptions extends CookieProperties {
|
|
|
36
111
|
secrets?: string[];
|
|
37
112
|
}
|
|
38
113
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
114
|
+
* Creates a new cookie object.
|
|
115
|
+
* @param name The name of the cookie
|
|
116
|
+
* @param options (optional) Additional options for the cookie
|
|
117
|
+
* @returns A cookie object
|
|
41
118
|
*/
|
|
42
|
-
export declare
|
|
43
|
-
|
|
44
|
-
constructor(name: string, options?: CookieOptions);
|
|
45
|
-
/**
|
|
46
|
-
* The name of the cookie.
|
|
47
|
-
*/
|
|
48
|
-
readonly name: string;
|
|
49
|
-
/**
|
|
50
|
-
* True if this cookie uses one or more secrets for verification.
|
|
51
|
-
*/
|
|
52
|
-
get signed(): boolean;
|
|
53
|
-
/**
|
|
54
|
-
* Extracts the value of this cookie from a `Cookie` header value.
|
|
55
|
-
* @param headerValue The value of the `Cookie` header to parse
|
|
56
|
-
* @returns The value of this cookie, or `null` if it's not present
|
|
57
|
-
*/
|
|
58
|
-
parse(headerValue: string | null): Promise<string | null>;
|
|
59
|
-
/**
|
|
60
|
-
* Returns the value to use in a `Set-Cookie` header for this cookie.
|
|
61
|
-
* @param value The value to serialize
|
|
62
|
-
* @param props (optional) Additional properties to use when serializing the cookie
|
|
63
|
-
* @returns The value to use in a `Set-Cookie` header for this cookie
|
|
64
|
-
*/
|
|
65
|
-
serialize(value: string, props?: CookieProperties): Promise<string>;
|
|
66
|
-
}
|
|
119
|
+
export declare function createCookie(name: string, options?: CookieOptions): Cookie;
|
|
120
|
+
export {};
|
|
67
121
|
//# sourceMappingURL=cookie.d.ts.map
|
package/dist/lib/cookie.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../../src/lib/cookie.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAA;AAI3B
|
|
1
|
+
{"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../../src/lib/cookie.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAA;AAI3B,KAAK,aAAa,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;AAE9C;;;;;;;GAOG;AACH,MAAM,WAAW,MAAM;IACrB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,IAAI,GAAG,SAAS,CAAA;IAClC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACzD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACnE;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAkF1E"}
|
package/dist/lib/cookie.js
CHANGED
|
@@ -1,76 +1,78 @@
|
|
|
1
1
|
import { Cookie as CookieHeader, SetCookie as SetCookieHeader, } from '@remix-run/headers';
|
|
2
|
-
import { sign, unsign } from "./
|
|
2
|
+
import { sign, unsign } from "./cookie-signing.js";
|
|
3
3
|
/**
|
|
4
|
-
* Creates a new
|
|
4
|
+
* Creates a new cookie object.
|
|
5
5
|
* @param name The name of the cookie
|
|
6
|
-
* @param options
|
|
7
|
-
* @returns A
|
|
6
|
+
* @param options (optional) Additional options for the cookie
|
|
7
|
+
* @returns A cookie object
|
|
8
8
|
*/
|
|
9
9
|
export function createCookie(name, options) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
export class Cookie {
|
|
17
|
-
constructor(name, options) {
|
|
18
|
-
let { decode = decodeURIComponent, encode = encodeURIComponent, secrets = [], ...props } = options ?? {};
|
|
19
|
-
this.name = name;
|
|
20
|
-
this.#decode = decode;
|
|
21
|
-
this.#encode = encode;
|
|
22
|
-
this.#secrets = secrets;
|
|
23
|
-
this.#props = props;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* The name of the cookie.
|
|
27
|
-
*/
|
|
28
|
-
name;
|
|
29
|
-
#decode;
|
|
30
|
-
#encode;
|
|
31
|
-
#secrets;
|
|
32
|
-
#props;
|
|
33
|
-
/**
|
|
34
|
-
* True if this cookie uses one or more secrets for verification.
|
|
35
|
-
*/
|
|
36
|
-
get signed() {
|
|
37
|
-
return this.#secrets.length > 0;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Extracts the value of this cookie from a `Cookie` header value.
|
|
41
|
-
* @param headerValue The value of the `Cookie` header to parse
|
|
42
|
-
* @returns The value of this cookie, or `null` if it's not present
|
|
43
|
-
*/
|
|
44
|
-
async parse(headerValue) {
|
|
45
|
-
if (!headerValue)
|
|
46
|
-
return null;
|
|
47
|
-
let header = new CookieHeader(headerValue);
|
|
48
|
-
if (!header.has(this.name))
|
|
49
|
-
return null;
|
|
50
|
-
let value = header.get(this.name);
|
|
51
|
-
if (value === '')
|
|
52
|
-
return '';
|
|
53
|
-
let decoded = await decodeCookieValue(value, this.#secrets, this.#decode);
|
|
54
|
-
return decoded;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Returns the value to use in a `Set-Cookie` header for this cookie.
|
|
58
|
-
* @param value The value to serialize
|
|
59
|
-
* @param props (optional) Additional properties to use when serializing the cookie
|
|
60
|
-
* @returns The value to use in a `Set-Cookie` header for this cookie
|
|
61
|
-
*/
|
|
62
|
-
async serialize(value, props) {
|
|
63
|
-
let header = new SetCookieHeader({
|
|
64
|
-
name: this.name,
|
|
65
|
-
value: value === '' ? '' : await encodeCookieValue(value, this.#secrets, this.#encode),
|
|
66
|
-
// sane defaults
|
|
67
|
-
path: '/',
|
|
68
|
-
sameSite: 'Lax',
|
|
69
|
-
...this.#props,
|
|
70
|
-
...props,
|
|
71
|
-
});
|
|
72
|
-
return header.toString();
|
|
10
|
+
let { decode = decodeURIComponent, encode = encodeURIComponent, secrets = [], domain, expires, httpOnly, maxAge, path = '/', partitioned, secure, sameSite = 'Lax', } = options ?? {};
|
|
11
|
+
if (partitioned === true) {
|
|
12
|
+
// Partitioned cookies must be set with Secure
|
|
13
|
+
// See https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies
|
|
14
|
+
secure = true;
|
|
73
15
|
}
|
|
16
|
+
return {
|
|
17
|
+
get domain() {
|
|
18
|
+
return domain;
|
|
19
|
+
},
|
|
20
|
+
get expires() {
|
|
21
|
+
return expires;
|
|
22
|
+
},
|
|
23
|
+
get httpOnly() {
|
|
24
|
+
return httpOnly ?? false;
|
|
25
|
+
},
|
|
26
|
+
get maxAge() {
|
|
27
|
+
return maxAge;
|
|
28
|
+
},
|
|
29
|
+
get name() {
|
|
30
|
+
return name;
|
|
31
|
+
},
|
|
32
|
+
async parse(headerValue) {
|
|
33
|
+
if (!headerValue)
|
|
34
|
+
return null;
|
|
35
|
+
let header = new CookieHeader(headerValue);
|
|
36
|
+
if (!header.has(name))
|
|
37
|
+
return null;
|
|
38
|
+
let value = header.get(name);
|
|
39
|
+
if (value === '')
|
|
40
|
+
return '';
|
|
41
|
+
let decoded = await decodeCookieValue(value, secrets, decode);
|
|
42
|
+
return decoded;
|
|
43
|
+
},
|
|
44
|
+
get partitioned() {
|
|
45
|
+
return partitioned ?? false;
|
|
46
|
+
},
|
|
47
|
+
get path() {
|
|
48
|
+
return path;
|
|
49
|
+
},
|
|
50
|
+
get sameSite() {
|
|
51
|
+
return sameSite;
|
|
52
|
+
},
|
|
53
|
+
get secure() {
|
|
54
|
+
return secure ?? false;
|
|
55
|
+
},
|
|
56
|
+
async serialize(value, props) {
|
|
57
|
+
let header = new SetCookieHeader({
|
|
58
|
+
name: name,
|
|
59
|
+
value: value === '' ? '' : await encodeCookieValue(value, secrets, encode),
|
|
60
|
+
domain,
|
|
61
|
+
expires,
|
|
62
|
+
httpOnly,
|
|
63
|
+
maxAge,
|
|
64
|
+
partitioned,
|
|
65
|
+
path,
|
|
66
|
+
sameSite,
|
|
67
|
+
secure,
|
|
68
|
+
...props,
|
|
69
|
+
});
|
|
70
|
+
return header.toString();
|
|
71
|
+
},
|
|
72
|
+
get signed() {
|
|
73
|
+
return secrets.length > 0;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
74
76
|
}
|
|
75
77
|
async function decodeCookieValue(value, secrets, decode) {
|
|
76
78
|
if (secrets.length > 0) {
|
|
@@ -123,9 +125,8 @@ function hex(code, length) {
|
|
|
123
125
|
}
|
|
124
126
|
async function encodeCookieValue(value, secrets, encode) {
|
|
125
127
|
let encoded = encodeValue(value, encode);
|
|
126
|
-
if (secrets.length > 0)
|
|
128
|
+
if (secrets.length > 0)
|
|
127
129
|
encoded = await sign(encoded, secrets[0]);
|
|
128
|
-
}
|
|
129
130
|
return encoded;
|
|
130
131
|
}
|
|
131
132
|
function encodeValue(value, encode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cookie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A toolkit for working with cookies in JavaScript",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@types/node": "^24.6.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@remix-run/headers": "^0.
|
|
32
|
+
"@remix-run/headers": "^0.17.0"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"http",
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { type Cookie, type CookieOptions, createCookie } from './lib/cookie.ts'
|
|
@@ -11,14 +11,18 @@ export async function sign(value: string, secret: string): Promise<string> {
|
|
|
11
11
|
|
|
12
12
|
export async function unsign(cookie: string, secret: string): Promise<string | false> {
|
|
13
13
|
let index = cookie.lastIndexOf('.')
|
|
14
|
+
|
|
15
|
+
if (index === -1) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
let value = cookie.slice(0, index)
|
|
15
20
|
let hash = cookie.slice(index + 1)
|
|
16
|
-
|
|
17
21
|
let data = encoder.encode(value)
|
|
18
|
-
|
|
19
22
|
let key = await createKey(secret, ['verify'])
|
|
23
|
+
|
|
20
24
|
try {
|
|
21
|
-
let signature =
|
|
25
|
+
let signature = byteStringToArray(atob(hash))
|
|
22
26
|
let valid = await crypto.subtle.verify('HMAC', key, signature, data)
|
|
23
27
|
|
|
24
28
|
return valid ? value : false
|
|
@@ -40,7 +44,7 @@ async function createKey(secret: string, usages: CryptoKey['usages']): Promise<C
|
|
|
40
44
|
)
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
function
|
|
47
|
+
function byteStringToArray(byteString: string): Uint8Array<ArrayBuffer> {
|
|
44
48
|
let array = new Uint8Array(byteString.length)
|
|
45
49
|
|
|
46
50
|
for (let i = 0; i < byteString.length; i++) {
|
package/src/lib/cookie.ts
CHANGED
|
@@ -4,16 +4,90 @@ import {
|
|
|
4
4
|
type CookieProperties,
|
|
5
5
|
} from '@remix-run/headers'
|
|
6
6
|
|
|
7
|
-
import { sign, unsign } from './
|
|
7
|
+
import { sign, unsign } from './cookie-signing.ts'
|
|
8
|
+
|
|
9
|
+
type SameSiteValue = 'Strict' | 'Lax' | 'None'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Represents a HTTP cookie.
|
|
13
|
+
*
|
|
14
|
+
* Supports parsing and serializing the cookie to/from `Cookie` and `Set-Cookie` headers.
|
|
15
|
+
*
|
|
16
|
+
* Also supports cryptographic signing of the cookie value to ensure it's not tampered with, and
|
|
17
|
+
* secret rotation to easily rotate secrets without breaking existing cookies.
|
|
14
18
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
19
|
+
export interface Cookie {
|
|
20
|
+
/**
|
|
21
|
+
* The domain of the cookie.
|
|
22
|
+
*
|
|
23
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#domaindomain-value)
|
|
24
|
+
*/
|
|
25
|
+
readonly domain: string | undefined
|
|
26
|
+
/**
|
|
27
|
+
* The expiration date of the cookie.
|
|
28
|
+
*
|
|
29
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#expiresdate)
|
|
30
|
+
*/
|
|
31
|
+
readonly expires: Date | undefined
|
|
32
|
+
/**
|
|
33
|
+
* True if the cookie is HTTP-only.
|
|
34
|
+
*
|
|
35
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#httponly)
|
|
36
|
+
*/
|
|
37
|
+
readonly httpOnly: boolean
|
|
38
|
+
/**
|
|
39
|
+
* The maximum age of the cookie in seconds.
|
|
40
|
+
*
|
|
41
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#max-agenumber)
|
|
42
|
+
*/
|
|
43
|
+
readonly maxAge: number | undefined
|
|
44
|
+
/**
|
|
45
|
+
* The name of the cookie.
|
|
46
|
+
*
|
|
47
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie-namecookie-value)
|
|
48
|
+
*/
|
|
49
|
+
readonly name: string
|
|
50
|
+
/**
|
|
51
|
+
* Extracts the value of this cookie from a `Cookie` header value.
|
|
52
|
+
* @param headerValue The `Cookie` header to parse
|
|
53
|
+
* @returns The value of this cookie, or `null` if it's not present
|
|
54
|
+
*/
|
|
55
|
+
parse(headerValue: string | null): Promise<string | null>
|
|
56
|
+
/**
|
|
57
|
+
* True if the cookie is partitioned.
|
|
58
|
+
*
|
|
59
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#partitioned)
|
|
60
|
+
*/
|
|
61
|
+
readonly partitioned: boolean
|
|
62
|
+
/**
|
|
63
|
+
* The path of the cookie. Defaults to `/`.
|
|
64
|
+
*
|
|
65
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value)
|
|
66
|
+
*/
|
|
67
|
+
readonly path: string
|
|
68
|
+
/**
|
|
69
|
+
* The `SameSite` attribute of the cookie. Defaults to `Lax`.
|
|
70
|
+
*
|
|
71
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)
|
|
72
|
+
*/
|
|
73
|
+
readonly sameSite: SameSiteValue
|
|
74
|
+
/**
|
|
75
|
+
* True if the cookie is secure (only sent over HTTPS).
|
|
76
|
+
*
|
|
77
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure)
|
|
78
|
+
*/
|
|
79
|
+
readonly secure: boolean
|
|
80
|
+
/**
|
|
81
|
+
* Returns the value to use in a `Set-Cookie` header for this cookie.
|
|
82
|
+
* @param value The value to serialize
|
|
83
|
+
* @param props (optional) Additional properties to use when serializing the cookie
|
|
84
|
+
* @returns The `Set-Cookie` header for this cookie
|
|
85
|
+
*/
|
|
86
|
+
serialize(value: string, props?: CookieProperties): Promise<string>
|
|
87
|
+
/**
|
|
88
|
+
* True if this cookie uses one or more secrets for verification.
|
|
89
|
+
*/
|
|
90
|
+
readonly signed: boolean
|
|
17
91
|
}
|
|
18
92
|
|
|
19
93
|
export interface CookieOptions extends CookieProperties {
|
|
@@ -47,85 +121,101 @@ export interface CookieOptions extends CookieProperties {
|
|
|
47
121
|
}
|
|
48
122
|
|
|
49
123
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
124
|
+
* Creates a new cookie object.
|
|
125
|
+
* @param name The name of the cookie
|
|
126
|
+
* @param options (optional) Additional options for the cookie
|
|
127
|
+
* @returns A cookie object
|
|
52
128
|
*/
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The name of the cookie.
|
|
71
|
-
*/
|
|
72
|
-
readonly name: string
|
|
73
|
-
|
|
74
|
-
readonly #decode: (value: string) => string
|
|
75
|
-
readonly #encode: (value: string) => string
|
|
76
|
-
readonly #secrets: string[]
|
|
77
|
-
readonly #props: CookieProperties
|
|
129
|
+
export function createCookie(name: string, options?: CookieOptions): Cookie {
|
|
130
|
+
let {
|
|
131
|
+
decode = decodeURIComponent,
|
|
132
|
+
encode = encodeURIComponent,
|
|
133
|
+
secrets = [],
|
|
134
|
+
domain,
|
|
135
|
+
expires,
|
|
136
|
+
httpOnly,
|
|
137
|
+
maxAge,
|
|
138
|
+
path = '/',
|
|
139
|
+
partitioned,
|
|
140
|
+
secure,
|
|
141
|
+
sameSite = 'Lax',
|
|
142
|
+
} = options ?? {}
|
|
78
143
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return this.#secrets.length > 0
|
|
144
|
+
if (partitioned === true) {
|
|
145
|
+
// Partitioned cookies must be set with Secure
|
|
146
|
+
// See https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies
|
|
147
|
+
secure = true
|
|
84
148
|
}
|
|
85
149
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
return {
|
|
151
|
+
get domain() {
|
|
152
|
+
return domain
|
|
153
|
+
},
|
|
154
|
+
get expires() {
|
|
155
|
+
return expires
|
|
156
|
+
},
|
|
157
|
+
get httpOnly() {
|
|
158
|
+
return httpOnly ?? false
|
|
159
|
+
},
|
|
160
|
+
get maxAge() {
|
|
161
|
+
return maxAge
|
|
162
|
+
},
|
|
163
|
+
get name() {
|
|
164
|
+
return name
|
|
165
|
+
},
|
|
166
|
+
async parse(headerValue: string | null): Promise<string | null> {
|
|
167
|
+
if (!headerValue) return null
|
|
93
168
|
|
|
94
|
-
|
|
95
|
-
|
|
169
|
+
let header = new CookieHeader(headerValue)
|
|
170
|
+
if (!header.has(name)) return null
|
|
96
171
|
|
|
97
|
-
|
|
98
|
-
|
|
172
|
+
let value = header.get(name)!
|
|
173
|
+
if (value === '') return ''
|
|
99
174
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
175
|
+
let decoded = await decodeCookieValue(value, secrets, decode)
|
|
176
|
+
return decoded
|
|
177
|
+
},
|
|
178
|
+
get partitioned() {
|
|
179
|
+
return partitioned ?? false
|
|
180
|
+
},
|
|
181
|
+
get path() {
|
|
182
|
+
return path
|
|
183
|
+
},
|
|
184
|
+
get sameSite() {
|
|
185
|
+
return sameSite
|
|
186
|
+
},
|
|
187
|
+
get secure() {
|
|
188
|
+
return secure ?? false
|
|
189
|
+
},
|
|
190
|
+
async serialize(value: string, props?: CookieProperties): Promise<string> {
|
|
191
|
+
let header = new SetCookieHeader({
|
|
192
|
+
name: name,
|
|
193
|
+
value: value === '' ? '' : await encodeCookieValue(value, secrets, encode),
|
|
194
|
+
domain,
|
|
195
|
+
expires,
|
|
196
|
+
httpOnly,
|
|
197
|
+
maxAge,
|
|
198
|
+
partitioned,
|
|
199
|
+
path,
|
|
200
|
+
sameSite,
|
|
201
|
+
secure,
|
|
202
|
+
...props,
|
|
203
|
+
})
|
|
103
204
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*/
|
|
110
|
-
async serialize(value: string, props?: CookieProperties): Promise<string> {
|
|
111
|
-
let header = new SetCookieHeader({
|
|
112
|
-
name: this.name,
|
|
113
|
-
value: value === '' ? '' : await encodeCookieValue(value, this.#secrets, this.#encode),
|
|
114
|
-
// sane defaults
|
|
115
|
-
path: '/',
|
|
116
|
-
sameSite: 'Lax',
|
|
117
|
-
...this.#props,
|
|
118
|
-
...props,
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
return header.toString()
|
|
205
|
+
return header.toString()
|
|
206
|
+
},
|
|
207
|
+
get signed() {
|
|
208
|
+
return secrets.length > 0
|
|
209
|
+
},
|
|
122
210
|
}
|
|
123
211
|
}
|
|
124
212
|
|
|
213
|
+
type Coder = (value: string) => string
|
|
214
|
+
|
|
125
215
|
async function decodeCookieValue(
|
|
126
216
|
value: string,
|
|
127
217
|
secrets: string[],
|
|
128
|
-
decode:
|
|
218
|
+
decode: Coder,
|
|
129
219
|
): Promise<string | null> {
|
|
130
220
|
if (secrets.length > 0) {
|
|
131
221
|
for (let secret of secrets) {
|
|
@@ -141,7 +231,7 @@ async function decodeCookieValue(
|
|
|
141
231
|
return decodeValue(value, decode)
|
|
142
232
|
}
|
|
143
233
|
|
|
144
|
-
function decodeValue(value: string, decode:
|
|
234
|
+
function decodeValue(value: string, decode: Coder): string | null {
|
|
145
235
|
try {
|
|
146
236
|
return decode(myEscape(atob(value)))
|
|
147
237
|
} catch {
|
|
@@ -177,21 +267,13 @@ function hex(code: number, length: number): string {
|
|
|
177
267
|
return result
|
|
178
268
|
}
|
|
179
269
|
|
|
180
|
-
async function encodeCookieValue(
|
|
181
|
-
value: string,
|
|
182
|
-
secrets: string[],
|
|
183
|
-
encode: (value: string) => string,
|
|
184
|
-
): Promise<string> {
|
|
270
|
+
async function encodeCookieValue(value: string, secrets: string[], encode: Coder): Promise<string> {
|
|
185
271
|
let encoded = encodeValue(value, encode)
|
|
186
|
-
|
|
187
|
-
if (secrets.length > 0) {
|
|
188
|
-
encoded = await sign(encoded, secrets[0])
|
|
189
|
-
}
|
|
190
|
-
|
|
272
|
+
if (secrets.length > 0) encoded = await sign(encoded, secrets[0])
|
|
191
273
|
return encoded
|
|
192
274
|
}
|
|
193
275
|
|
|
194
|
-
function encodeValue(value: string, encode:
|
|
276
|
+
function encodeValue(value: string, encode: Coder): string {
|
|
195
277
|
return btoa(myUnescape(encode(value)))
|
|
196
278
|
}
|
|
197
279
|
|
package/dist/lib/crypto.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/lib/crypto.ts"],"names":[],"mappings":"AAEA,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzE;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,CAmBpF"}
|