@remix-run/cookie 0.2.0 → 0.4.0

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 CHANGED
@@ -1,4 +1,4 @@
1
- # @remix-run/cookie
1
+ # cookie
2
2
 
3
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
4
 
@@ -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,9 @@ npm install @remix-run/cookie
21
19
  ## Usage
22
20
 
23
21
  ```tsx
24
- import { Cookie } from '@remix-run/cookie'
22
+ import { createCookie } from '@remix-run/cookie'
25
23
 
26
- let sessionCookie = new Cookie('session')
24
+ let sessionCookie = createCookie('session', { secrets: ['s3cret1'] })
27
25
 
28
26
  // Get the value of the "session" cookie from the request's `Cookie` header
29
27
  let value = await sessionCookie.parse(request.headers.get('Cookie'))
@@ -46,7 +44,7 @@ Secret rotation is also supported, so you can easily rotate in new secrets witho
46
44
  import { Cookie } from '@remix-run/cookie'
47
45
 
48
46
  // Start with a single secret
49
- let sessionCookie = new Cookie('session', {
47
+ let sessionCookie = createCookie('session', {
50
48
  secrets: ['secret1'],
51
49
  })
52
50
 
@@ -62,12 +60,19 @@ let response = new Response('Hello, world!', {
62
60
  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
61
 
64
62
  ```tsx
65
- let sessionCookie = new Cookie('session', {
63
+ let sessionCookie = createCookie('session', {
66
64
  secrets: ['secret2', 'secret1'],
67
65
  })
68
66
 
69
- // This will still work for cookies signed with the old secret
67
+ // This works for cookies signed with either secret
70
68
  let value = await sessionCookie.parse(request.headers.get('Cookie'))
69
+
70
+ // Newly serialized cookies will be signed with the new secret
71
+ let response = new Response('Hello, world!', {
72
+ headers: {
73
+ 'Set-Cookie': await sessionCookie.serialize(value),
74
+ },
75
+ })
71
76
  ```
72
77
 
73
78
  ### Custom Encoding
@@ -75,7 +80,7 @@ let value = await sessionCookie.parse(request.headers.get('Cookie'))
75
80
  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
81
 
77
82
  ```tsx
78
- let sessionCookie = new Cookie('session', {
83
+ let sessionCookie = createCookie('session', {
79
84
  encode: (value) => value,
80
85
  decode: (value) => value,
81
86
  })
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { type CookieOptions, Cookie } from './lib/cookie.ts';
1
+ export { type Cookie, type CookieOptions, createCookie } from './lib/cookie.ts';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA"}
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 { Cookie } from "./lib/cookie.js";
1
+ export { createCookie } from "./lib/cookie.js";
@@ -1,3 +1,3 @@
1
1
  export declare function sign(value: string, secret: string): Promise<string>;
2
2
  export declare function unsign(cookie: string, secret: string): Promise<string | false>;
3
- //# sourceMappingURL=crypto.d.ts.map
3
+ //# sourceMappingURL=cookie-signing.d.ts.map
@@ -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 = byteStringToUint8Array(atob(hash));
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 byteStringToUint8Array(byteString) {
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);
@@ -1,5 +1,87 @@
1
1
  import { type CookieProperties } from '@remix-run/headers';
2
- export interface CookieOptions {
2
+ type SameSiteValue = 'Strict' | 'Lax' | 'None';
3
+ /**
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.
10
+ */
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
+ }
84
+ export interface CookieOptions extends CookieProperties {
3
85
  /**
4
86
  * A function that decodes the cookie value.
5
87
  *
@@ -29,29 +111,11 @@ export interface CookieOptions {
29
111
  secrets?: string[];
30
112
  }
31
113
  /**
32
- * A container for metadata about a HTTP cookie; its name and secrets that may be used
33
- * to sign/unsign the value of the cookie to ensure it's not tampered with.
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
34
118
  */
35
- export declare class Cookie {
36
- #private;
37
- readonly name: string;
38
- constructor(name: string, options?: CookieOptions);
39
- /**
40
- * True if this cookie uses one or more secrets for verification.
41
- */
42
- get isSigned(): boolean;
43
- /**
44
- * Extracts the value of this cookie from a `Cookie` header value.
45
- * @param headerValue The value of the `Cookie` header to parse
46
- * @returns The value of this cookie, or `null` if it's not present
47
- */
48
- parse(headerValue: string | null): Promise<string | null>;
49
- /**
50
- * Returns the value to use in a `Set-Cookie` header for this cookie.
51
- * @param value The value to serialize
52
- * @param props (optional) Additional properties to use when serializing the cookie
53
- * @returns The value to use in a `Set-Cookie` header for this cookie
54
- */
55
- serialize(value: string, props?: CookieProperties): Promise<string>;
56
- }
119
+ export declare function createCookie(name: string, options?: CookieOptions): Cookie;
120
+ export {};
57
121
  //# sourceMappingURL=cookie.d.ts.map
@@ -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,MAAM,WAAW,aAAa;IAC5B;;;;;;;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;;;GAGG;AACH,qBAAa,MAAM;;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;gBAKT,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa;IAOjD;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;;OAIG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAa/D;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;CAY1E"}
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,CA4E1E"}
@@ -1,72 +1,75 @@
1
1
  import { Cookie as CookieHeader, SetCookie as SetCookieHeader, } from '@remix-run/headers';
2
- import { sign, unsign } from "./crypto.js";
2
+ import { sign, unsign } from "./cookie-signing.js";
3
3
  /**
4
- * A container for metadata about a HTTP cookie; its name and secrets that may be used
5
- * to sign/unsign the value of the cookie to ensure it's not tampered with.
4
+ * Creates a new cookie object.
5
+ * @param name The name of the cookie
6
+ * @param options (optional) Additional options for the cookie
7
+ * @returns A cookie object
6
8
  */
7
- export class Cookie {
8
- name;
9
- #decode;
10
- #encode;
11
- #secrets;
12
- constructor(name, options) {
13
- this.name = name;
14
- this.#decode = options?.decode;
15
- this.#encode = options?.encode;
16
- this.#secrets = options?.secrets ?? [];
17
- }
18
- /**
19
- * True if this cookie uses one or more secrets for verification.
20
- */
21
- get isSigned() {
22
- return this.#secrets.length > 0;
23
- }
24
- /**
25
- * Extracts the value of this cookie from a `Cookie` header value.
26
- * @param headerValue The value of the `Cookie` header to parse
27
- * @returns The value of this cookie, or `null` if it's not present
28
- */
29
- async parse(headerValue) {
30
- if (!headerValue)
31
- return null;
32
- let header = new CookieHeader(headerValue);
33
- if (!header.has(this.name))
34
- return null;
35
- let value = header.get(this.name);
36
- if (value === '')
37
- return '';
38
- let decoded = await decodeCookieValue(value, this.#secrets, this.#decode);
39
- return decoded;
40
- }
41
- /**
42
- * Returns the value to use in a `Set-Cookie` header for this cookie.
43
- * @param value The value to serialize
44
- * @param props (optional) Additional properties to use when serializing the cookie
45
- * @returns The value to use in a `Set-Cookie` header for this cookie
46
- */
47
- async serialize(value, props) {
48
- let header = new SetCookieHeader({
49
- name: this.name,
50
- value: value === '' ? '' : await encodeCookieValue(value, this.#secrets, this.#encode),
51
- // sane defaults
52
- path: '/',
53
- sameSite: 'Lax',
54
- ...props,
55
- });
56
- return header.toString();
57
- }
58
- }
59
- async function encodeCookieValue(value, secrets, encode = encodeURIComponent) {
60
- let encoded = encodeValue(value, encode);
61
- if (secrets.length > 0) {
62
- encoded = await sign(encoded, secrets[0]);
63
- }
64
- return encoded;
9
+ export function createCookie(name, options) {
10
+ let { decode = decodeURIComponent, encode = encodeURIComponent, secrets = [], domain, expires, httpOnly, maxAge, path = '/', partitioned, secure, sameSite = 'Lax', } = options ?? {};
11
+ return {
12
+ get domain() {
13
+ return domain;
14
+ },
15
+ get expires() {
16
+ return expires;
17
+ },
18
+ get httpOnly() {
19
+ return httpOnly ?? false;
20
+ },
21
+ get maxAge() {
22
+ return maxAge;
23
+ },
24
+ get name() {
25
+ return name;
26
+ },
27
+ async parse(headerValue) {
28
+ if (!headerValue)
29
+ return null;
30
+ let header = new CookieHeader(headerValue);
31
+ if (!header.has(name))
32
+ return null;
33
+ let value = header.get(name);
34
+ if (value === '')
35
+ return '';
36
+ let decoded = await decodeCookieValue(value, secrets, decode);
37
+ return decoded;
38
+ },
39
+ get partitioned() {
40
+ return partitioned ?? false;
41
+ },
42
+ get path() {
43
+ return path;
44
+ },
45
+ get sameSite() {
46
+ return sameSite;
47
+ },
48
+ get secure() {
49
+ return secure ?? false;
50
+ },
51
+ async serialize(value, props) {
52
+ let header = new SetCookieHeader({
53
+ name: name,
54
+ value: value === '' ? '' : await encodeCookieValue(value, secrets, encode),
55
+ domain,
56
+ expires,
57
+ httpOnly,
58
+ maxAge,
59
+ partitioned,
60
+ path,
61
+ sameSite,
62
+ secure,
63
+ ...props,
64
+ });
65
+ return header.toString();
66
+ },
67
+ get signed() {
68
+ return secrets.length > 0;
69
+ },
70
+ };
65
71
  }
66
- function encodeValue(value, encode) {
67
- return btoa(myUnescape(encode(value)));
68
- }
69
- async function decodeCookieValue(value, secrets, decode = decodeURIComponent) {
72
+ async function decodeCookieValue(value, secrets, decode) {
70
73
  if (secrets.length > 0) {
71
74
  for (let secret of secrets) {
72
75
  let unsignedValue = await unsign(value, secret);
@@ -115,6 +118,15 @@ function hex(code, length) {
115
118
  result = '0' + result;
116
119
  return result;
117
120
  }
121
+ async function encodeCookieValue(value, secrets, encode) {
122
+ let encoded = encodeValue(value, encode);
123
+ if (secrets.length > 0)
124
+ encoded = await sign(encoded, secrets[0]);
125
+ return encoded;
126
+ }
127
+ function encodeValue(value, encode) {
128
+ return btoa(myUnescape(encode(value)));
129
+ }
118
130
  // See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.unescape.js
119
131
  function myUnescape(value) {
120
132
  let str = value.toString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/cookie",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
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.15.0"
32
+ "@remix-run/headers": "^0.16.0"
33
33
  },
34
34
  "keywords": [
35
35
  "http",
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { type CookieOptions, Cookie } from './lib/cookie.ts'
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 = byteStringToUint8Array(atob(hash))
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 byteStringToUint8Array(byteString: string): Uint8Array {
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,9 +4,93 @@ import {
4
4
  type CookieProperties,
5
5
  } from '@remix-run/headers'
6
6
 
7
- import { sign, unsign } from './crypto.ts'
7
+ import { sign, unsign } from './cookie-signing.ts'
8
8
 
9
- export interface CookieOptions {
9
+ type SameSiteValue = 'Strict' | 'Lax' | 'None'
10
+
11
+ /**
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.
18
+ */
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
91
+ }
92
+
93
+ export interface CookieOptions extends CookieProperties {
10
94
  /**
11
95
  * A function that decodes the cookie value.
12
96
  *
@@ -37,89 +121,95 @@ export interface CookieOptions {
37
121
  }
38
122
 
39
123
  /**
40
- * A container for metadata about a HTTP cookie; its name and secrets that may be used
41
- * to sign/unsign the value of the cookie to ensure it's not tampered with.
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
42
128
  */
43
- export class Cookie {
44
- readonly name: string
45
- readonly #decode?: (value: string) => string
46
- readonly #encode?: (value: string) => string
47
- readonly #secrets: string[]
48
-
49
- constructor(name: string, options?: CookieOptions) {
50
- this.name = name
51
- this.#decode = options?.decode
52
- this.#encode = options?.encode
53
- this.#secrets = options?.secrets ?? []
54
- }
55
-
56
- /**
57
- * True if this cookie uses one or more secrets for verification.
58
- */
59
- get isSigned(): boolean {
60
- return this.#secrets.length > 0
61
- }
62
-
63
- /**
64
- * Extracts the value of this cookie from a `Cookie` header value.
65
- * @param headerValue The value of the `Cookie` header to parse
66
- * @returns The value of this cookie, or `null` if it's not present
67
- */
68
- async parse(headerValue: string | null): Promise<string | null> {
69
- if (!headerValue) return null
70
-
71
- let header = new CookieHeader(headerValue)
72
- if (!header.has(this.name)) return null
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 ?? {}
73
143
 
74
- let value = header.get(this.name)!
75
- if (value === '') return ''
144
+ return {
145
+ get domain() {
146
+ return domain
147
+ },
148
+ get expires() {
149
+ return expires
150
+ },
151
+ get httpOnly() {
152
+ return httpOnly ?? false
153
+ },
154
+ get maxAge() {
155
+ return maxAge
156
+ },
157
+ get name() {
158
+ return name
159
+ },
160
+ async parse(headerValue: string | null): Promise<string | null> {
161
+ if (!headerValue) return null
76
162
 
77
- let decoded = await decodeCookieValue(value, this.#secrets, this.#decode)
78
- return decoded
79
- }
163
+ let header = new CookieHeader(headerValue)
164
+ if (!header.has(name)) return null
80
165
 
81
- /**
82
- * Returns the value to use in a `Set-Cookie` header for this cookie.
83
- * @param value The value to serialize
84
- * @param props (optional) Additional properties to use when serializing the cookie
85
- * @returns The value to use in a `Set-Cookie` header for this cookie
86
- */
87
- async serialize(value: string, props?: CookieProperties): Promise<string> {
88
- let header = new SetCookieHeader({
89
- name: this.name,
90
- value: value === '' ? '' : await encodeCookieValue(value, this.#secrets, this.#encode),
91
- // sane defaults
92
- path: '/',
93
- sameSite: 'Lax',
94
- ...props,
95
- })
96
-
97
- return header.toString()
98
- }
99
- }
166
+ let value = header.get(name)!
167
+ if (value === '') return ''
100
168
 
101
- async function encodeCookieValue(
102
- value: string,
103
- secrets: string[],
104
- encode: (value: string) => string = encodeURIComponent,
105
- ): Promise<string> {
106
- let encoded = encodeValue(value, encode)
169
+ let decoded = await decodeCookieValue(value, secrets, decode)
170
+ return decoded
171
+ },
172
+ get partitioned() {
173
+ return partitioned ?? false
174
+ },
175
+ get path() {
176
+ return path
177
+ },
178
+ get sameSite() {
179
+ return sameSite
180
+ },
181
+ get secure() {
182
+ return secure ?? false
183
+ },
184
+ async serialize(value: string, props?: CookieProperties): Promise<string> {
185
+ let header = new SetCookieHeader({
186
+ name: name,
187
+ value: value === '' ? '' : await encodeCookieValue(value, secrets, encode),
188
+ domain,
189
+ expires,
190
+ httpOnly,
191
+ maxAge,
192
+ partitioned,
193
+ path,
194
+ sameSite,
195
+ secure,
196
+ ...props,
197
+ })
107
198
 
108
- if (secrets.length > 0) {
109
- encoded = await sign(encoded, secrets[0])
199
+ return header.toString()
200
+ },
201
+ get signed() {
202
+ return secrets.length > 0
203
+ },
110
204
  }
111
-
112
- return encoded
113
205
  }
114
206
 
115
- function encodeValue(value: string, encode: (value: string) => string): string {
116
- return btoa(myUnescape(encode(value)))
117
- }
207
+ type Coder = (value: string) => string
118
208
 
119
209
  async function decodeCookieValue(
120
210
  value: string,
121
211
  secrets: string[],
122
- decode: (value: string) => string = decodeURIComponent,
212
+ decode: Coder,
123
213
  ): Promise<string | null> {
124
214
  if (secrets.length > 0) {
125
215
  for (let secret of secrets) {
@@ -135,7 +225,7 @@ async function decodeCookieValue(
135
225
  return decodeValue(value, decode)
136
226
  }
137
227
 
138
- function decodeValue(value: string, decode: (value: string) => string): string | null {
228
+ function decodeValue(value: string, decode: Coder): string | null {
139
229
  try {
140
230
  return decode(myEscape(atob(value)))
141
231
  } catch {
@@ -171,6 +261,16 @@ function hex(code: number, length: number): string {
171
261
  return result
172
262
  }
173
263
 
264
+ async function encodeCookieValue(value: string, secrets: string[], encode: Coder): Promise<string> {
265
+ let encoded = encodeValue(value, encode)
266
+ if (secrets.length > 0) encoded = await sign(encoded, secrets[0])
267
+ return encoded
268
+ }
269
+
270
+ function encodeValue(value: string, encode: Coder): string {
271
+ return btoa(myUnescape(encode(value)))
272
+ }
273
+
174
274
  // See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.unescape.js
175
275
  function myUnescape(value: string): string {
176
276
  let str = value.toString()
@@ -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"}