shelving 1.137.0 → 1.138.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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.137.0",
14
+ "version": "1.138.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
package/util/base64.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- /** Encode a string to Base64 (with no `=` padding on the end). */
2
- export declare function encodeBase64(str: string): string;
3
- /** Decode a string from Base64 (strips `=` padding on the end). */
4
- export declare function decodeBase64(b64: string): string;
5
- /** Encode a string to URL-safe Base64 */
6
- export declare function encodeBase64Url(str: string): string;
1
+ import { type PossibleBytes } from "./bytes.js";
2
+ /** Encode a string or binary data to Base64 string. */
3
+ export declare function encodeBase64(input: PossibleBytes, pad?: boolean): string;
4
+ /** Decode Base64 string to string. */
5
+ export declare function decodeBase64String(base64: string): string;
6
+ /** Decode URL-safe Base64 string to binary data (as a UInt8Array). */
7
+ export declare function decodeBase64Bytes(base64: string): Uint8Array;
8
+ /** Encode a string or binary data to URL-safe Base64 */
9
+ export declare function encodeBase64Url(input: PossibleBytes, pad?: boolean): string;
7
10
  /** Decode a string from URL-safe Base64. */
8
- export declare function decodeBase64Url(b64: string): string;
11
+ export declare function decodeBase64UrlString(base64: string): string;
12
+ /** Decode URL-safe Base64 string to binary data (as a UInt8Array). */
13
+ export declare function decodeBase64UrlBytes(base64: string): Uint8Array;
package/util/base64.js CHANGED
@@ -1,16 +1,77 @@
1
- /** Encode a string to Base64 (with no `=` padding on the end). */
2
- export function encodeBase64(str) {
3
- return btoa(str).replace(/=+$/, "");
1
+ import { requireBytes } from "./bytes.js";
2
+ const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ const BASE64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
+ /**
5
+ * @todo DH: When it's well supported, use `Uint8Array.toBase64()`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64
6
+ */
7
+ function _encode(bytes, alphabet, pad) {
8
+ const len = bytes.length;
9
+ let output = "";
10
+ for (let i = 0; i < len; i += 3) {
11
+ const b1 = bytes[i];
12
+ const b2 = i + 1 < len ? bytes[i + 1] : 0;
13
+ const b3 = i + 2 < len ? bytes[i + 2] : 0;
14
+ const combined = (b1 << 16) | (b2 << 8) | b3;
15
+ const c1 = (combined >> 18) & 0x3f;
16
+ const c2 = (combined >> 12) & 0x3f;
17
+ const c3 = (combined >> 6) & 0x3f;
18
+ const c4 = combined & 0x3f;
19
+ output += `${alphabet[c1]}${alphabet[c2]}${i + 1 < len ? alphabet[c3] : pad}${i + 2 < len ? alphabet[c4] : pad}`;
20
+ }
21
+ return output;
4
22
  }
5
- /** Decode a string from Base64 (strips `=` padding on the end). */
6
- export function decodeBase64(b64) {
7
- return atob(b64.replace(/=+$/, ""));
23
+ function _decode(base64, alphabet) {
24
+ // Create a reverse lookup table: char -> 6-bit value
25
+ const values = new Uint8Array(128);
26
+ for (let i = 0; i < alphabet.length; i++)
27
+ values[alphabet.charCodeAt(i)] = i;
28
+ // Remove padding.
29
+ const cleaned = base64.replace(/=+$/, "");
30
+ const length = cleaned.length;
31
+ // Calculate output byte length
32
+ // Every 4 base64 chars = 3 bytes; adjust for padding
33
+ const outputLength = Math.floor((length * 6) / 8);
34
+ const output = new Uint8Array(outputLength);
35
+ let j = 0;
36
+ for (let i = 0; i < length; i += 4) {
37
+ // Get 4 characters (or less at the end)
38
+ const c1 = values[cleaned.charCodeAt(i)];
39
+ const c2 = values[cleaned.charCodeAt(i + 1)];
40
+ const c3 = i + 2 < length ? values[cleaned.charCodeAt(i + 2)] : 0;
41
+ const c4 = i + 3 < length ? values[cleaned.charCodeAt(i + 3)] : 0;
42
+ // Combine into 24 bits
43
+ const combined = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
44
+ // Extract bytes and add to output if within range
45
+ if (j < outputLength)
46
+ output[j++] = (combined >> 16) & 0xff;
47
+ if (j < outputLength)
48
+ output[j++] = (combined >> 8) & 0xff;
49
+ if (j < outputLength)
50
+ output[j++] = combined & 0xff;
51
+ }
52
+ return output;
8
53
  }
9
- /** Encode a string to URL-safe Base64 */
10
- export function encodeBase64Url(str) {
11
- return encodeBase64(str).replace(/\+/g, "-").replace(/\//g, "_");
54
+ /** Encode a string or binary data to Base64 string. */
55
+ export function encodeBase64(input, pad = true) {
56
+ return _encode(requireBytes(input), BASE64_CHARS, pad ? "=" : "");
57
+ }
58
+ /** Decode Base64 string to string. */
59
+ export function decodeBase64String(base64) {
60
+ return new TextDecoder("utf-8").decode(_decode(base64, BASE64_CHARS));
61
+ }
62
+ /** Decode URL-safe Base64 string to binary data (as a UInt8Array). */
63
+ export function decodeBase64Bytes(base64) {
64
+ return _decode(base64, BASE64_CHARS);
65
+ }
66
+ /** Encode a string or binary data to URL-safe Base64 */
67
+ export function encodeBase64Url(input, pad = false) {
68
+ return _encode(requireBytes(input), BASE64URL_CHARS, pad ? "=" : "");
12
69
  }
13
70
  /** Decode a string from URL-safe Base64. */
14
- export function decodeBase64Url(b64) {
15
- return decodeBase64(b64.replace(/-/g, "+").replace(/_/g, "/"));
71
+ export function decodeBase64UrlString(base64) {
72
+ return new TextDecoder("utf-8").decode(_decode(base64, BASE64URL_CHARS));
73
+ }
74
+ /** Decode URL-safe Base64 string to binary data (as a UInt8Array). */
75
+ export function decodeBase64UrlBytes(base64) {
76
+ return _decode(base64, BASE64URL_CHARS);
16
77
  }
package/util/buffer.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- /** Detect if an unknown value is an `ArrayBuffer` (not a view like `Uint8Array` or `Float32Array` etc). */
1
+ export type TypedArray<T extends ArrayBufferLike = ArrayBufferLike> = Uint8Array<T> | Uint16Array<T> | Uint32Array<T> | Int8Array<T> | Int16Array<T> | Int32Array<T> | Float32Array<T> | Float64Array<T>;
2
+ /** Detect if an unknown value is an `ArrayBuffer` (not a view like `Uint8Array` or `Float32Array` or `DataView`). */
2
3
  export declare function isBuffer(value: unknown): value is ArrayBuffer;
3
- /** Detect if an unknown value is an `ArrayBuffer` view like `Uint8Array` or `Float32Array` etc. */
4
+ /** Detect if an unknown value is an `ArrayBufferView`, like `Uint8Array` or `Float32Array` or `DataView` */
4
5
  export declare function isBufferView(value: unknown): value is ArrayBufferView;
5
- /** Encode a string as a `Uint8Array` */
6
- export declare function encodeBufferView(str: string): Uint8Array;
7
- /** Encode `Uint8Array` to a string. */
8
- export declare function decodeBufferView(buffer: ArrayBuffer | ArrayBufferView): string;
6
+ /** Detect if an unknown value is a `TypedArray`, like `Uint8Array` or `Float32Array` (not including `DataView`). */
7
+ export declare function isTypedArray(value: unknown): value is TypedArray;
package/util/buffer.js CHANGED
@@ -1,24 +1,12 @@
1
- import { ValueError } from "../error/ValueError.js";
2
- /** Detect if an unknown value is an `ArrayBuffer` (not a view like `Uint8Array` or `Float32Array` etc). */
1
+ /** Detect if an unknown value is an `ArrayBuffer` (not a view like `Uint8Array` or `Float32Array` or `DataView`). */
3
2
  export function isBuffer(value) {
4
3
  return value instanceof ArrayBuffer;
5
4
  }
6
- /** Detect if an unknown value is an `ArrayBuffer` view like `Uint8Array` or `Float32Array` etc. */
5
+ /** Detect if an unknown value is an `ArrayBufferView`, like `Uint8Array` or `Float32Array` or `DataView` */
7
6
  export function isBufferView(value) {
8
7
  return ArrayBuffer.isView(value);
9
8
  }
10
- /** Encode a string as a `Uint8Array` */
11
- export function encodeBufferView(str) {
12
- return new TextEncoder().encode(str);
13
- }
14
- /** Encode `Uint8Array` to a string. */
15
- export function decodeBufferView(buffer) {
16
- try {
17
- return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
18
- }
19
- catch (thrown) {
20
- if (thrown instanceof TypeError)
21
- throw new ValueError(thrown.message, { received: buffer, caller: decodeBufferView });
22
- throw thrown;
23
- }
9
+ /** Detect if an unknown value is a `TypedArray`, like `Uint8Array` or `Float32Array` (not including `DataView`). */
10
+ export function isTypedArray(value) {
11
+ return value instanceof Object.getPrototypeOf(Uint8Array);
24
12
  }
@@ -0,0 +1,12 @@
1
+ /** Types that can be converted to a `Uint8Array` byte sequence. */
2
+ export type PossibleBytes = Uint8Array | ArrayBuffer | string;
3
+ /**
4
+ * Convert an unknown value to a `Uint8Array` byte sequence, or `undefined` if the value cannot be converted.
5
+ *
6
+ * - ArrayBuffers and TypedArrays are converted to `Uint8Array`
7
+ * - Strings are encoded as UTF-8.
8
+ * - Everything else returns `undefined`
9
+ */
10
+ export declare function getBytes(value: unknown): Uint8Array | undefined;
11
+ /** Convert an unknown value to a `Uint8Array` byte sequence, or throw `RequiredError` if the value cannot be converted. */
12
+ export declare function requireBytes(value: PossibleBytes): Uint8Array;
package/util/bytes.js ADDED
@@ -0,0 +1,24 @@
1
+ import { RequiredError } from "../error/RequiredError.js";
2
+ /**
3
+ * Convert an unknown value to a `Uint8Array` byte sequence, or `undefined` if the value cannot be converted.
4
+ *
5
+ * - ArrayBuffers and TypedArrays are converted to `Uint8Array`
6
+ * - Strings are encoded as UTF-8.
7
+ * - Everything else returns `undefined`
8
+ */
9
+ export function getBytes(value) {
10
+ if (value instanceof Uint8Array)
11
+ return value;
12
+ if (value instanceof ArrayBuffer)
13
+ return new Uint8Array(value);
14
+ if (typeof value === "string")
15
+ return new TextEncoder().encode(value);
16
+ return undefined;
17
+ }
18
+ /** Convert an unknown value to a `Uint8Array` byte sequence, or throw `RequiredError` if the value cannot be converted. */
19
+ export function requireBytes(value) {
20
+ const bytes = getBytes(value);
21
+ if (bytes === undefined)
22
+ throw new RequiredError("Value cannot be converted to byte array", { received: value, caller: requireBytes });
23
+ return bytes;
24
+ }
package/util/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./async.js";
3
3
  export * from "./base64.js";
4
4
  export * from "./boolean.js";
5
5
  export * from "./buffer.js";
6
+ export * from "./bytes.js";
6
7
  export * from "./callback.js";
7
8
  export * from "./class.js";
8
9
  export * from "./color.js";
@@ -26,6 +27,7 @@ export * from "./hydrate.js";
26
27
  export * from "./item.js";
27
28
  export * from "./iterate.js";
28
29
  export * from "./jsx.js";
30
+ export * from "./jwt.js";
29
31
  export * from "./lazy.js";
30
32
  export * from "./link.js";
31
33
  export * from "./map.js";
package/util/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./async.js";
3
3
  export * from "./base64.js";
4
4
  export * from "./boolean.js";
5
5
  export * from "./buffer.js";
6
+ export * from "./bytes.js";
6
7
  export * from "./callback.js";
7
8
  export * from "./class.js";
8
9
  export * from "./color.js";
@@ -26,6 +27,7 @@ export * from "./hydrate.js";
26
27
  export * from "./item.js";
27
28
  export * from "./iterate.js";
28
29
  export * from "./jsx.js";
30
+ export * from "./jwt.js";
29
31
  export * from "./lazy.js";
30
32
  export * from "./link.js";
31
33
  export * from "./map.js";
package/util/jwt.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Data } from "./data.js";
2
+ import type { AnyFunction } from "./function.js";
3
+ /**
4
+ * Encode a JWT and return the string token.
5
+ * - Currently only supports HMAC SHA-512 signing.
6
+ *
7
+ * @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
8
+ */
9
+ export declare function encodeToken(claims: Data, secret: string): Promise<string>;
10
+ /** Parts that make up a JSON Web Token. */
11
+ export type TokenData = {
12
+ header: string;
13
+ payload: string;
14
+ signature: string;
15
+ headerData: Data;
16
+ payloadData: Data;
17
+ };
18
+ /**
19
+ * Split a JSON Web Token into its header, payload, and signature, and decode and parse the JSON.
20
+ */
21
+ export declare function splitToken(token: unknown): TokenData;
22
+ export declare function _splitToken(caller: AnyFunction, token: unknown): TokenData;
23
+ /**
24
+ * Decode a JWT, verify it, and return the full payload data.
25
+ * - Currently only supports HMAC SHA-512 signing.
26
+ *
27
+ * @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
28
+ * @throws RequestError If the token is invalid or malformed.
29
+ * @throws UnauthorizedError If the token signature is incorrect, token is expired or not issued yet.
30
+ */
31
+ export declare function verifyToken(token: unknown, secret: string): Promise<Data>;
package/util/jwt.js ADDED
@@ -0,0 +1,101 @@
1
+ import { RequestError, UnauthorizedError } from "../error/RequestError.js";
2
+ import { ValueError } from "../error/ValueError.js";
3
+ import { decodeBase64UrlBytes, decodeBase64UrlString, encodeBase64Url } from "./base64.js";
4
+ import { requireBytes } from "./bytes.js";
5
+ import { DAY } from "./constants.js";
6
+ // Constants.
7
+ const TOKEN_HEADER = { alg: "HS512", typ: "JWT" };
8
+ const TOKEN_EXPIRY_MS = DAY * 10;
9
+ const TOKEN_MINIMUM_SECRET = 16;
10
+ function _getKey(secret, ...usages) {
11
+ return crypto.subtle.importKey("raw", requireBytes(secret), { name: "HMAC", hash: { name: "SHA-512" } }, false, usages);
12
+ }
13
+ /**
14
+ * Encode a JWT and return the string token.
15
+ * - Currently only supports HMAC SHA-512 signing.
16
+ *
17
+ * @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
18
+ */
19
+ export async function encodeToken(claims, secret) {
20
+ if (typeof secret !== "string" || secret.length < TOKEN_MINIMUM_SECRET)
21
+ throw new ValueError(`JWT secret must be string with minimum ${TOKEN_MINIMUM_SECRET} characters`, {
22
+ received: secret,
23
+ caller: encodeToken,
24
+ });
25
+ // Encode header.
26
+ const header = encodeBase64Url(JSON.stringify(TOKEN_HEADER));
27
+ // Encode payload.
28
+ const iat = Math.floor(Date.now() / 1000);
29
+ const exp = Math.floor(iat + TOKEN_EXPIRY_MS / 1000);
30
+ const payload = encodeBase64Url(JSON.stringify({ iat, exp, ...claims }));
31
+ // Create signature.
32
+ const key = await _getKey(secret, "sign");
33
+ const signature = encodeBase64Url(await crypto.subtle.sign("HMAC", key, requireBytes(`${header}.${payload}`)));
34
+ // Combine token.
35
+ return `${header}.${payload}.${signature}`;
36
+ }
37
+ /**
38
+ * Split a JSON Web Token into its header, payload, and signature, and decode and parse the JSON.
39
+ */
40
+ export function splitToken(token) {
41
+ return _splitToken(splitToken, token);
42
+ }
43
+ export function _splitToken(caller, token) {
44
+ if (typeof token !== "string")
45
+ throw new RequestError("JWT token must be string", { received: token, caller });
46
+ // Split token.
47
+ const [header, payload, signature] = token.split(".");
48
+ if (!header || !payload || !signature)
49
+ throw new RequestError("JWT token must have header, payload, and signature", { received: token, caller });
50
+ // Decode header.
51
+ let headerData;
52
+ try {
53
+ headerData = JSON.parse(decodeBase64UrlString(header));
54
+ }
55
+ catch {
56
+ throw new RequestError("JWT token header must be base64Url encoded JSON", { received: token, caller });
57
+ }
58
+ // Decode payload.
59
+ let payloadData;
60
+ try {
61
+ payloadData = JSON.parse(decodeBase64UrlString(payload));
62
+ }
63
+ catch {
64
+ throw new RequestError("JWT token payload must be base64Url encoded JSON", { received: token, caller });
65
+ }
66
+ return { header, payload, headerData, payloadData, signature };
67
+ }
68
+ /**
69
+ * Decode a JWT, verify it, and return the full payload data.
70
+ * - Currently only supports HMAC SHA-512 signing.
71
+ *
72
+ * @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
73
+ * @throws RequestError If the token is invalid or malformed.
74
+ * @throws UnauthorizedError If the token signature is incorrect, token is expired or not issued yet.
75
+ */
76
+ export async function verifyToken(token, secret) {
77
+ if (typeof secret !== "string" || secret.length < TOKEN_MINIMUM_SECRET)
78
+ throw new ValueError(`JWT secret must be string with minimum ${TOKEN_MINIMUM_SECRET} characters`, {
79
+ received: secret,
80
+ caller: verifyToken,
81
+ });
82
+ const { header, payload, signature, headerData, payloadData } = _splitToken(verifyToken, token);
83
+ // Validate header.
84
+ if (headerData.typ !== TOKEN_HEADER.typ)
85
+ throw new RequestError(`JWT header type must be \"${TOKEN_HEADER.typ}\"`, { received: headerData.typ, caller: verifyToken });
86
+ if (headerData.alg !== TOKEN_HEADER.alg)
87
+ throw new RequestError(`JWT header algorithm must be \"${TOKEN_HEADER.alg}\"`, { received: headerData.alg, caller: verifyToken });
88
+ // Validate signature.
89
+ const key = await _getKey(secret, "verify");
90
+ const isValid = await crypto.subtle.verify("HMAC", key, decodeBase64UrlBytes(signature), requireBytes(`${header}.${payload}`));
91
+ if (!isValid)
92
+ throw new UnauthorizedError("JWT signature does not match", { received: token, caller: verifyToken });
93
+ // Validate payload.
94
+ const { iat, exp } = payloadData;
95
+ const now = Math.floor(Date.now() / 1000);
96
+ if (typeof iat === "number" && iat > now)
97
+ throw new UnauthorizedError("JWT not issued yet", { received: iat, caller: verifyToken });
98
+ if (typeof exp === "number" && exp < now)
99
+ throw new UnauthorizedError("JWT has expired", { received: exp, caller: verifyToken });
100
+ return payloadData;
101
+ }