@rexezuge/shared 1.0.6 → 1.0.7

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
@@ -12,18 +12,18 @@ pnpm add @rexezuge/shared
12
12
 
13
13
  ## 1. What's inside
14
14
 
15
- | Area | Exports |
16
- | --- | --- |
17
- | Byte codecs | `bytesToBase64`, `base64ToBytes` |
18
- | Time | `SystemClock` / `FixedClock` (injectable `Clock`), `TimestampUtil`, `TimeZoneUtil` + `DEFAULT_TIME_ZONE` |
19
- | Locale | `LocaleUtil` (`DEFAULT_LOCALE`, `SUPPORTED_LOCALES`, display names), `canonicalizeLanguageTag` |
20
- | Email | `normalizeEmail`, `isValidEmailFormat`, `MAX_EMAIL_LENGTH` |
21
- | Identifiers | `UUIDUtil`, `CryptoIdGenerator` / `FixedIdGenerator` (injectable `IdGenerator`) |
22
- | Crypto + secrets | `CryptoUtil`, `TokenHashUtil`, `passwordFingerprint` |
23
- | Error handling | `ErrorSanitizationUtil` (operator/client message split), `ok` / `err` / `getOrThrow` / `mapResult` + `Result` |
24
- | Network policy | `normalizeRemoteUrl`, `resolveRedirectUrl`, `parseAllowedHosts`, `isPrivateOrInternalHost` (+ limits and blocklists) |
25
- | Pagination | `CursorCodec` (opaque cursors, `Page`) |
26
- | Subrequest accounting | `SubrequestCounter`, `SUBREQUEST_KINDS`, `subrequestSpend` (Cloudflare's 50-subrequest ceiling) |
15
+ | Area | Exports |
16
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------- |
17
+ | Byte codecs | `bytesToBase64`, `base64ToBytes` |
18
+ | Time | `SystemClock` / `FixedClock` (injectable `Clock`), `TimestampUtil`, `TimeZoneUtil` + `DEFAULT_TIME_ZONE` |
19
+ | Locale | `LocaleUtil` (`DEFAULT_LOCALE`, `SUPPORTED_LOCALES`, display names), `canonicalizeLanguageTag` |
20
+ | Email | `normalizeEmail`, `isValidEmailFormat`, `MAX_EMAIL_LENGTH` |
21
+ | Identifiers | `UUIDUtil`, `CryptoIdGenerator` / `FixedIdGenerator` (injectable `IdGenerator`) |
22
+ | Crypto + secrets | `CryptoUtil`, `TokenHashUtil`, `passwordFingerprint` |
23
+ | Error handling | `ErrorSanitizationUtil` (operator/client message split), `ok` / `err` / `getOrThrow` / `mapResult` + `Result` |
24
+ | Network policy | `normalizeRemoteUrl`, `resolveRedirectUrl`, `parseAllowedHosts`, `isPrivateOrInternalHost` (+ limits and blocklists) |
25
+ | Pagination | `CursorCodec` (opaque cursors, `Page`) |
26
+ | Subrequest accounting | `SubrequestCounter`, `SUBREQUEST_KINDS`, `subrequestSpend` (Cloudflare's 50-subrequest ceiling) |
27
27
 
28
28
  ## 2. Usage
29
29
 
@@ -1 +1 @@
1
- {"version":3,"file":"CryptoUtil.d.ts","sourceRoot":"","sources":["../src/CryptoUtil.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,cAAM,UAAU;IACd;;;;;;;OAOG;WACiB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7D;;;;;;OAMG;WACiB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxE;;;;;;;OAOG;WACiB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYjF,OAAO,CAAC,MAAM,CAAC,KAAK;IAIpB;;;;;;OAMG;WACW,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAIpD;;;;;;OAMG;WACW,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;WAItC,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAG1D;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"CryptoUtil.d.ts","sourceRoot":"","sources":["../src/CryptoUtil.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,cAAM,UAAU;IACd;;;;;;;OAOG;WACiB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7D;;;;;;OAMG;WACiB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxE;;;;;;;OAOG;WACiB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYjF,OAAO,CAAC,MAAM,CAAC,KAAK;IAIpB;;;;;;OAMG;WACW,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAOpD;;;;;;OAMG;WACW,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;WAItC,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAG1D;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -77,7 +77,10 @@ class CryptoUtil {
77
77
  * `Uint8Array#toBase64` is not guaranteed across Workers runtimes.
78
78
  */
79
79
  static toBase64Url(bytes) {
80
- return bytesToBase64(bytes).replaceAll('+', '-').replaceAll('/', '_').replace(/={0,2}$/, '');
80
+ return bytesToBase64(bytes)
81
+ .replaceAll('+', '-')
82
+ .replaceAll('/', '_')
83
+ .replace(/={0,2}$/, '');
81
84
  }
82
85
  /**
83
86
  * URL-safe base64 of a UTF-8 string.
@@ -1 +1 @@
1
- {"version":3,"file":"CryptoUtil.js","sourceRoot":"","sources":["../src/CryptoUtil.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU;IACd;;;;;;;OAOG;IACI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,KAAa;QACzC,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAiB;QACpD,MAAM,MAAM,GAAgB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAqB,CAAC,CAAC;QACzF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;OAOG;IACI,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,KAAa,EAAE,MAAc;QAC7D,MAAM,GAAG,GAAc,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAClD,KAAK,EACL,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAChC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,SAAS,GAAgB,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtG,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/C,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,KAAiB;QACpC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,WAAW,CAAC,KAAiB;QACzC,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,eAAe,CAAC,KAAa;QACzC,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEM,MAAM,CAAC,eAAe,CAAC,UAAkB;QAC9C,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;CACF;AAED,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import { bytesToBase64 } from './Base64';\n\n/**\n * Hashing and key generation.\n *\n * ## Provenance\n *\n * Converged from `CryptoUtil.ts` in Durable-DAV, Edge-Git, Mail-Meow,\n * Mail-Otter, ChordDHT-Tracker and CalDAV-Bridge. Every copy carries\n * `sha256Hex` and `randomBase64Url`; Edge-Git/Mail-Meow/Mail-Otter/ChordDHT add\n * `hmacSha256Hex`; Mail-Meow adds the string overload `base64UrlEncode`;\n * Durable-DAV uniquely factors the digest over raw bytes (`sha256HexOfBytes`) and\n * rewrites base64url on the shared codec.\n *\n * ## Design decisions\n *\n * - The base64url alphabet is a **different alphabet for a different job** from\n * `Base64.ts`, so the URL-safe transform stays here — but it delegates to the\n * documented best encoder rather than re-spreading bytes (`String.fromCodePoint`\n * + `btoa`), which was four copies of the ~2×-UTF-16 form `Base64.ts` replaced.\n * - `sha256Hex` is defined over `sha256HexOfBytes` (Durable-DAV's shape): the\n * digest then covers exactly the bytes presented, which matters when two sides\n * compare a digest — an intermediate encoding is a place for them to disagree.\n * - `hmacSha256Hex` is kept from the four repos that carry it.\n *\n * ## Left local\n *\n * ChordDHT-Tracker's `sha1Hex` is deliberately **not** here. SHA-1 is the Chord\n * ring identifier hash (protocol compatibility), not a general security digest;\n * surfacing it in the shared surface would invite exactly the misuse the\n * `sonarjs/hashing` lint it carries a disable for is there to catch. A repo that\n * needs a protocol-mandated SHA-1 computes it on a `BufferSource` itself.\n */\nclass CryptoUtil {\n /**\n * SHA-256 of a string, hex-encoded.\n *\n * Encoded as UTF-8 first. For bytes you already hold, prefer `sha256HexOfBytes`\n * — this overload is the reason a byte payload used to be laundered through\n * `String.fromCodePoint` into a binary string and back, allocating roughly 2×\n * the input as UTF-16 on the way.\n */\n public static async sha256Hex(value: string): Promise<string> {\n return this.sha256HexOfBytes(new TextEncoder().encode(value));\n }\n\n /**\n * SHA-256 of raw bytes, hex-encoded.\n *\n * The hash covers exactly these bytes. This matters where the digest is\n * compared between two sides: any intermediate encoding is a place for the two\n * to disagree for a reason that has nothing to do with the content.\n */\n public static async sha256HexOfBytes(bytes: Uint8Array): Promise<string> {\n const digest: ArrayBuffer = await crypto.subtle.digest('SHA-256', bytes as BufferSource);\n return this.toHex(new Uint8Array(digest));\n }\n\n /**\n * HMAC-SHA256 of `value` under `secret`, hex-encoded.\n *\n * A keyed MAC for a message a caller will recompute on the other side. The key\n * is imported as non-extractable; a comparison against a re-derived MAC must be\n * timing-safe on the caller's side — this helper returns the hex string, it does\n * not compare.\n */\n public static async hmacSha256Hex(value: string, secret: string): Promise<string> {\n const key: CryptoKey = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature: ArrayBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value));\n return this.toHex(new Uint8Array(signature));\n }\n\n private static toHex(bytes: Uint8Array): string {\n return Array.from(bytes, (byte: number): string => byte.toString(16).padStart(2, '0')).join('');\n }\n\n /**\n * URL-safe base64 of `bytes` (no `+`, `/` or `=`).\n *\n * Delegates to the shared codec rather than rebuilding the binary string here:\n * the URL-safe transform is applied to the standard alphabet because\n * `Uint8Array#toBase64` is not guaranteed across Workers runtimes.\n */\n public static toBase64Url(bytes: Uint8Array): string {\n return bytesToBase64(bytes).replaceAll('+', '-').replaceAll('/', '_').replace(/={0,2}$/, '');\n }\n\n /**\n * URL-safe base64 of a UTF-8 string.\n *\n * The same transform as {@link toBase64Url}, for callers holding text rather\n * than bytes. This and `toBase64Url` used to be separate implementations in\n * different packages; they are the same encoding and must not drift.\n */\n public static base64UrlEncode(value: string): string {\n return this.toBase64Url(new TextEncoder().encode(value));\n }\n\n public static randomBase64Url(byteLength: number): string {\n return this.toBase64Url(crypto.getRandomValues(new Uint8Array(byteLength)));\n }\n}\n\nexport { CryptoUtil };\n"]}
1
+ {"version":3,"file":"CryptoUtil.js","sourceRoot":"","sources":["../src/CryptoUtil.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU;IACd;;;;;;;OAOG;IACI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,KAAa;QACzC,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAiB;QACpD,MAAM,MAAM,GAAgB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAqB,CAAC,CAAC;QACzF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;OAOG;IACI,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,KAAa,EAAE,MAAc;QAC7D,MAAM,GAAG,GAAc,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAClD,KAAK,EACL,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAChC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;QACF,MAAM,SAAS,GAAgB,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtG,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/C,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,KAAiB;QACpC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,WAAW,CAAC,KAAiB;QACzC,OAAO,aAAa,CAAC,KAAK,CAAC;aACxB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;aACpB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;aACpB,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,eAAe,CAAC,KAAa;QACzC,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEM,MAAM,CAAC,eAAe,CAAC,UAAkB;QAC9C,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;CACF;AAED,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import { bytesToBase64 } from './Base64';\n\n/**\n * Hashing and key generation.\n *\n * ## Provenance\n *\n * Converged from `CryptoUtil.ts` in Durable-DAV, Edge-Git, Mail-Meow,\n * Mail-Otter, ChordDHT-Tracker and CalDAV-Bridge. Every copy carries\n * `sha256Hex` and `randomBase64Url`; Edge-Git/Mail-Meow/Mail-Otter/ChordDHT add\n * `hmacSha256Hex`; Mail-Meow adds the string overload `base64UrlEncode`;\n * Durable-DAV uniquely factors the digest over raw bytes (`sha256HexOfBytes`) and\n * rewrites base64url on the shared codec.\n *\n * ## Design decisions\n *\n * - The base64url alphabet is a **different alphabet for a different job** from\n * `Base64.ts`, so the URL-safe transform stays here — but it delegates to the\n * documented best encoder rather than re-spreading bytes (`String.fromCodePoint`\n * + `btoa`), which was four copies of the ~2×-UTF-16 form `Base64.ts` replaced.\n * - `sha256Hex` is defined over `sha256HexOfBytes` (Durable-DAV's shape): the\n * digest then covers exactly the bytes presented, which matters when two sides\n * compare a digest — an intermediate encoding is a place for them to disagree.\n * - `hmacSha256Hex` is kept from the four repos that carry it.\n *\n * ## Left local\n *\n * ChordDHT-Tracker's `sha1Hex` is deliberately **not** here. SHA-1 is the Chord\n * ring identifier hash (protocol compatibility), not a general security digest;\n * surfacing it in the shared surface would invite exactly the misuse the\n * `sonarjs/hashing` lint it carries a disable for is there to catch. A repo that\n * needs a protocol-mandated SHA-1 computes it on a `BufferSource` itself.\n */\nclass CryptoUtil {\n /**\n * SHA-256 of a string, hex-encoded.\n *\n * Encoded as UTF-8 first. For bytes you already hold, prefer `sha256HexOfBytes`\n * — this overload is the reason a byte payload used to be laundered through\n * `String.fromCodePoint` into a binary string and back, allocating roughly 2×\n * the input as UTF-16 on the way.\n */\n public static async sha256Hex(value: string): Promise<string> {\n return this.sha256HexOfBytes(new TextEncoder().encode(value));\n }\n\n /**\n * SHA-256 of raw bytes, hex-encoded.\n *\n * The hash covers exactly these bytes. This matters where the digest is\n * compared between two sides: any intermediate encoding is a place for the two\n * to disagree for a reason that has nothing to do with the content.\n */\n public static async sha256HexOfBytes(bytes: Uint8Array): Promise<string> {\n const digest: ArrayBuffer = await crypto.subtle.digest('SHA-256', bytes as BufferSource);\n return this.toHex(new Uint8Array(digest));\n }\n\n /**\n * HMAC-SHA256 of `value` under `secret`, hex-encoded.\n *\n * A keyed MAC for a message a caller will recompute on the other side. The key\n * is imported as non-extractable; a comparison against a re-derived MAC must be\n * timing-safe on the caller's side — this helper returns the hex string, it does\n * not compare.\n */\n public static async hmacSha256Hex(value: string, secret: string): Promise<string> {\n const key: CryptoKey = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature: ArrayBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value));\n return this.toHex(new Uint8Array(signature));\n }\n\n private static toHex(bytes: Uint8Array): string {\n return Array.from(bytes, (byte: number): string => byte.toString(16).padStart(2, '0')).join('');\n }\n\n /**\n * URL-safe base64 of `bytes` (no `+`, `/` or `=`).\n *\n * Delegates to the shared codec rather than rebuilding the binary string here:\n * the URL-safe transform is applied to the standard alphabet because\n * `Uint8Array#toBase64` is not guaranteed across Workers runtimes.\n */\n public static toBase64Url(bytes: Uint8Array): string {\n return bytesToBase64(bytes)\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/={0,2}$/, '');\n }\n\n /**\n * URL-safe base64 of a UTF-8 string.\n *\n * The same transform as {@link toBase64Url}, for callers holding text rather\n * than bytes. This and `toBase64Url` used to be separate implementations in\n * different packages; they are the same encoding and must not drift.\n */\n public static base64UrlEncode(value: string): string {\n return this.toBase64Url(new TextEncoder().encode(value));\n }\n\n public static randomBase64Url(byteLength: number): string {\n return this.toBase64Url(crypto.getRandomValues(new Uint8Array(byteLength)));\n }\n}\n\nexport { CryptoUtil };\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteUrlPolicy.d.ts","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,QAAA,MAAM,qBAAqB,OAAO,CAAC;AACnC,QAAA,MAAM,aAAa,IAAI,CAAC;AAExB;;GAEG;AACH,QAAA,MAAM,qBAAqB,8DAA+D,CAAC;AAE3F;;GAEG;AACH,QAAA,MAAM,iBAAiB,aAAmF,CAAC;AAE3G,KAAK,gBAAgB,GAAG;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEF,cAAM,sBAAuB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AA8GD;;GAEG;AACH,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,iBAAS,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa1D;AAED;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA4ChF;AAED;;;;;;;GAOG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAgBrG;AAED,iBAAS,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAKjD;AAED,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,CAAC;AACF,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"RemoteUrlPolicy.d.ts","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,QAAA,MAAM,qBAAqB,OAAO,CAAC;AACnC,QAAA,MAAM,aAAa,IAAI,CAAC;AAExB;;GAEG;AACH,QAAA,MAAM,qBAAqB,8DAA+D,CAAC;AAE3F;;GAEG;AACH,QAAA,MAAM,iBAAiB,aAAmF,CAAC;AAE3G,KAAK,gBAAgB,GAAG;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEF,cAAM,sBAAuB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AA8GD;;GAEG;AACH,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,iBAAS,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa1D;AAED;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA+ChF;AAED;;;;;;;GAOG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAgBrG;AAED,iBAAS,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAOjD;AAED,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,CAAC;AACF,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
@@ -330,10 +330,12 @@ function resolveRedirectUrl(baseUrl, location, options = {}) {
330
330
  return normalizeRemoteUrl(next.href, options);
331
331
  }
332
332
  function parseAllowedHosts(raw) {
333
- return typeof raw !== 'string' || raw.trim() === '' ? [] : raw
334
- .split(',')
335
- .map((entry) => entry.trim())
336
- .filter((entry) => entry !== '');
333
+ return typeof raw !== 'string' || raw.trim() === ''
334
+ ? []
335
+ : raw
336
+ .split(',')
337
+ .map((entry) => entry.trim())
338
+ .filter((entry) => entry !== '');
337
339
  }
338
340
  export { normalizeRemoteUrl, resolveRedirectUrl, parseAllowedHosts, isPrivateOrInternalHost, stripBrackets, stripTrailingDot, RemoteUrlRejectedError, MAX_REMOTE_URL_LENGTH, MAX_REDIRECTS, BLOCKED_HOST_SUFFIXES, BLOCKED_HOSTNAMES, };
339
341
  //# sourceMappingURL=RemoteUrlPolicy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteUrlPolicy.js","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Egress policy and SSRF host predicates for user-supplied URLs.\n *\n * ## Provenance\n *\n * Merged from Durable-DAV's `packages/shared/src/net/RemoteUrlPolicy.ts` and the\n * `SsrfHosts.ts` of Durable-DAV-Router, Edge-Git and Edge-Sonic. The two were the\n * same problem solved twice: Durable-DAV's `RemoteUrlPolicy` is the full\n * validate-and-canonicalize policy (HTTPS-only, public-address-only, operator\n * allow-list, redirect re-validation); the Router/Edge-Git/Edge-Sonic\n * `SsrfHosts` are the lower-level \"is this host private?\" predicate the policy in\n * each repo leaned on. Canonical name is **`RemoteUrlPolicy`** (Durable-DAV's);\n * the host predicate it converges onto is exported as **`isPrivateOrInternalHost`**\n * (the common `isSsrfBlockedHost`-style composite from SsrfHosts).\n *\n * Scheduled replication / webhook delivery / git import turns a caller into\n * someone who can name a host this Worker connects to. Without a policy that is\n * server-side request forgery against Cloudflare's own egress: the metadata\n * service, the loopback interface, and anything reachable on a private network the\n * Worker happens to share.\n *\n * ## Design decisions\n *\n * - **The octet-accurate IP checks win over SsrfHosts' naive string prefixes.**\n * SsrfHosts' `isBlockedIpv6Host` matched `startsWith('fc')`/`'fd')`, which also\n * matches a *hostname* like `fcb.example.com`; Durable-DAV's `isBlockedIpv6` /\n * `isBlockedIpv4` parse the address (and decode IPv4-mapped IPv6) instead. The\n * kit keeps the rigorous form for both the URL policy and the exported\n * predicate, so there is one correct answer rather than two that drift.\n * - **`isPrivateOrInternalHost` is strictly safer than either source.** It folds\n * in SsrfHosts' raw-hostname handling (bare-integer/octal/hex/`0x`-dotted forms,\n * trailing-dot and bracket stripping, localhost names and `.local`/`.internal`/\n * `.home.arpa` suffixes) that Durable-DAV's URL-path checks could skip because\n * `new URL` had already canonicalized the host. A public IPv6 literal is allowed\n * exactly as `normalizeRemoteUrl` allows it; every private/loopback/unique-local/\n * link-local/zone/mapped form is refused.\n *\n * ## Known gap, stated rather than hidden\n *\n * DNS rebinding is not covered. A host that resolves to a public address at\n * validation time and a private one when the connection is made defeats every\n * check here. Closing it needs a resolution-aware fetch, which the platform does\n * not expose to a Worker. The upheld rule for such a deployment is to pin an\n * origin allow-list.\n */\n\nconst MAX_REMOTE_URL_LENGTH = 2048;\nconst MAX_REDIRECTS = 3;\n\n/**\n * Host suffixes that never name a real internet peer.\n */\nconst BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.home.arpa'] as const;\n\n/**\n * Hostnames that resolve inward by definition.\n */\nconst BLOCKED_HOSTNAMES = new Set(['localhost', 'localhost.localdomain', 'ip6-localhost', 'ip6-loopback']);\n\ntype UrlPolicyOptions = {\n /**\n * Hostnames (exact, or `.suffix` form) permitted regardless of the address\n * rules. Only ever populated from operator configuration, never from the\n * request body.\n */\n allowedHosts?: readonly string[];\n};\n\nclass RemoteUrlRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteUrlRejectedError';\n }\n}\n\nfunction isIpv4Literal(host: string): boolean {\n return /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host);\n}\n\nfunction ipv4Octets(host: string): number[] | null {\n const parts = host.split('.');\n if (parts.length !== 4) return null;\n const octets: number[] = [];\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const value = Number(part);\n if (value > 255) return null;\n octets.push(value);\n }\n return octets;\n}\n\n/**\n * Is this IPv4 literal in a range a Worker must never reach?\n *\n * `0.0.0.0/8` and `169.254.0.0/16` matter as much as RFC 1918: the first is\n * \"this host\" on Linux, the second is the cloud metadata endpoint.\n */\nfunction isBlockedIpv4(octets: number[]): boolean {\n const [a = 0, b = 0] = octets;\n if ([0, 10, 127].includes(a)) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n return false;\n}\n\n/**\n * Decode an IPv4-mapped IPv6 address back into its four octets, or null.\n *\n * Both spellings have to be handled. `new URL` canonicalizes\n * `https://[::ffff:10.0.0.1]/` to the hex form `[::ffff:a00:1]`, so a check\n * written only against the dotted text never fires — and that address connects to\n * a private network.\n */\nfunction mappedIpv4Octets(literal: string): number[] | null {\n const dotted = /^::ffff:\\d{1,3}(?:\\.\\d{1,3}){3}$/.exec(literal);\n const dottedText = literal.slice('::ffff:'.length);\n if (dotted !== null) return ipv4Octets(dottedText);\n const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(literal);\n if (!hex?.[1] || !hex[2]) return null;\n const high = Number.parseInt(hex[1], 16);\n const low = Number.parseInt(hex[2], 16);\n return !Number.isFinite(high) || !Number.isFinite(low) ? null : [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff];\n}\n\n/**\n * Is this IPv6 literal private, loopback, or an IPv4 address in disguise? `literal`\n * is the **unbracketed** inner form (e.g. `::1`, `fe80::1`).\n *\n * The last case is the one that matters: `::ffff:127.0.0.1` is a perfectly valid\n * URL and connects to loopback, so the embedded IPv4 must be re-checked rather\n * than the literal waved through because it \"is\" IPv6.\n */\nfunction isBlockedIpv6(literal: string): boolean {\n const lower = literal.toLowerCase();\n if (lower === '::' || lower === '::1') return true;\n // Zone ids (`fe80::1%eth0`) name a local interface directly.\n if (lower.includes('%')) return true;\n const mapped = mappedIpv4Octets(lower);\n if (mapped !== null) return isBlockedIpv4(mapped);\n const head = lower.split(':', 1)[0] ?? '';\n if (head === '') return false;\n const group = Number.parseInt(head.padStart(4, '0').slice(0, 4), 16);\n if (!Number.isFinite(group)) return false;\n // fc00::/7 unique-local, fe80::/10 link-local.\n return (group & 0xfe_00) === 0xfc_00 || (group & 0xff_c0) === 0xfe_80;\n}\n\n/**\n * Does this host name a numeric address in a non-canonical spelling? The strict\n * superset of Durable-DAV's `looksLikeObfuscatedNumericHost` and SsrfHosts'\n * `isEncodedNumericHost`, for the raw-hostname predicate.\n *\n * `2130706433`, `0177.0.0.1`, `0x7f000001` and `0x7f.1` all name `127.0.0.1`.\n * Through `new URL` these arrive already canonicalized (so `isBlockedIpv4` catches\n * them); a hostname handed to `isPrivateOrInternalHost` need not have gone through\n * `URL`, so a bare integer/octal/hex form is caught here directly. A legitimate\n * internet hostname is never all-digits, so over-blocking is not a risk.\n */\nfunction looksLikeObfuscatedNumericHost(host: string): boolean {\n if (isIpv4Literal(host) || host.startsWith('[') || !/^[0-9a-fx.]+$/i.test(host)) return false;\n return (\n /^0x[0-9a-f]+$/i.test(host) ||\n /^\\d+$/.test(host) ||\n /^0[0-7]+(?:\\.0[0-7]+)*$/.test(host) ||\n // An all-digits-and-dots host that is not a canonical dotted-quad — e.g.\n // `0177.0.0.1` or `0200.0250.0001.0001` — is an obfuscated numeric address.\n /^[\\d.]+$/.test(host) ||\n host.split('.').some((group) => /^0x[0-9a-f]+$/i.test(group))\n );\n}\n\nfunction hostIsAllowed(host: string, allowedHosts: readonly string[]): boolean {\n const lower = host.toLowerCase();\n return allowedHosts.some((entry) => {\n const candidate = entry.trim().toLowerCase();\n if (candidate === '') return false;\n return candidate.startsWith('.') ? lower.endsWith(candidate) : lower === candidate;\n });\n}\n\n/**\n * Strip one enclosing `[`…`]` pair (an IPv6 literal's URL brackets).\n */\nfunction stripBrackets(host: string): string {\n const h = host.trim();\n return h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h;\n}\n\n/**\n * Strip a bracketed wrapper and every trailing root-label dot.\n *\n * A fully-qualified name ends in a root label (`example.com.`) and `new URL` keeps\n * it; RFC 4918 §9.1.2 clients vary on sending it, so the canonical form is compared.\n */\nfunction stripTrailingDot(host: string): string {\n const unbracketed = stripBrackets(host);\n let end = unbracketed.length;\n while (end > 0 && unbracketed.charAt(end - 1) === '.') end -= 1;\n return unbracketed.slice(0, end);\n}\n\n/**\n * Host names that mean \"this machine or a local suffix\", not an internet peer.\n */\nfunction isLocalhostName(host: string): boolean {\n return BLOCKED_HOSTNAMES.has(host) || host.endsWith('.localhost');\n}\n\n/**\n * True when a URL host resolves to this machine or a private network.\n *\n * Intended for user-supplied URLs that the server will subsequently fetch. Such a\n * URL turns the process into a proxy into its own network: without this, a\n * user-supplied `baseUrl` can point at cloud metadata, a database host, or a\n * loopback admin port, and the response is readable by the caller.\n *\n * Encoded-numeric hosts are included because `http://2130706433/` and\n * `http://0177.0.0.1/` are ordinary-looking strings that name `127.0.0.1` after\n * parsing.\n *\n * A *strict, safer* predicate: an empty host, any private/loopback/link-local/\n * unique-local/mapped form, any localhost/suffix name, any obfuscated numeric\n * form, and any `:`-bearing host that is not a public IPv6 literal all return\n * true. It is a *syntactic* check — a public hostname can still resolve to a\n * private address (DNS rebinding) — so a hard-guarantee deployment additionally\n * pins an allow-list (see the file header).\n */\nfunction isPrivateOrInternalHost(hostname: string): boolean {\n const host = stripTrailingDot(hostname).toLowerCase();\n if (host.length === 0) return true;\n if (isLocalhostName(host)) return true;\n if (BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))) return true;\n\n if (isIpv4Literal(host)) {\n const octets = ipv4Octets(host);\n return octets === null || isBlockedIpv4(octets);\n }\n if (host.includes(':')) return isBlockedIpv6(host);\n if (looksLikeObfuscatedNumericHost(host)) return true;\n return false;\n}\n\n/**\n * Validate and canonicalise a remote URL.\n *\n * Throws `RemoteUrlRejectedError` with a message meant for the owner: every\n * rejection here is a configuration mistake or an attack, and saying which one\n * is what makes the difference between a fixable setup error and a support\n * ticket. Query and fragment are dropped — a WebDAV base URL has no use for\n * either, and keeping them would let `?a=b` smuggle a second meaning past the\n * path checks the sync engine does.\n */\nfunction normalizeRemoteUrl(raw: unknown, options: UrlPolicyOptions = {}): string {\n if (typeof raw !== 'string') throw new RemoteUrlRejectedError('remoteUrl must be a string');\n const trimmed = raw.trim();\n if (trimmed === '') throw new RemoteUrlRejectedError('remoteUrl is required');\n if (trimmed.length > MAX_REMOTE_URL_LENGTH) throw new RemoteUrlRejectedError(`remoteUrl must be at most ${MAX_REMOTE_URL_LENGTH} characters`);\n\n let url: URL;\n try {\n url = new URL(trimmed);\n } catch {\n throw new RemoteUrlRejectedError('remoteUrl is not a valid absolute URL');\n }\n\n if (url.protocol !== 'https:') throw new RemoteUrlRejectedError('remoteUrl must use https');\n if (url.username !== '' || url.password !== '') throw new RemoteUrlRejectedError('remoteUrl must not embed credentials; use the auth fields instead');\n if (url.port !== '' && url.port !== '443') throw new RemoteUrlRejectedError('remoteUrl must use the default https port');\n\n const host = url.hostname.toLowerCase();\n if (host === '') throw new RemoteUrlRejectedError('remoteUrl must include a host');\n if (hostIsAllowed(host, options.allowedHosts ?? [])) {\n url.search = '';\n url.hash = '';\n return url.href;\n }\n\n if (BLOCKED_HOSTNAMES.has(host)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not routable`);\n for (const suffix of BLOCKED_HOST_SUFFIXES) {\n if (host.endsWith(suffix)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public hostname`);\n }\n if (looksLikeObfuscatedNumericHost(host)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is a numeric address in a non-canonical form`);\n\n if (isIpv4Literal(host)) {\n const octets = ipv4Octets(host);\n if (octets === null || isBlockedIpv4(octets)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);\n } else if (host.startsWith('[')) {\n if (isBlockedIpv6(host.slice(1, -1))) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);\n } else if (!host.includes('.')) {\n // A single-label name can only be resolved through a local search domain.\n throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a fully qualified hostname`);\n }\n\n url.search = '';\n url.hash = '';\n return url.href;\n}\n\n/**\n * Re-validate the target of a redirect.\n *\n * Called for every hop rather than once at the start. Validating the configured\n * URL and then following `Location` unchecked is a classic TOCTOU: the first\n * hop is public, the second is `169.254.169.254`, and the policy has already\n * returned by then.\n */\nfunction resolveRedirectUrl(baseUrl: string, location: string, options: UrlPolicyOptions = {}): string {\n let next: URL;\n try {\n next = new URL(location, baseUrl);\n } catch {\n throw new RemoteUrlRejectedError('redirect target is not a valid URL');\n }\n if (next.protocol !== 'https:') throw new RemoteUrlRejectedError('redirect target must use https');\n // Same host: no need to re-run the whole policy, and a redirect back to the\n // origin's own host is the overwhelmingly common case.\n if (next.host.toLowerCase() === new URL(baseUrl).host.toLowerCase()) {\n next.search = '';\n next.hash = '';\n return next.href;\n }\n return normalizeRemoteUrl(next.href, options);\n}\n\nfunction parseAllowedHosts(raw: unknown): string[] {\n return typeof raw !== 'string' || raw.trim() === '' ? [] : raw\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry !== '');\n}\n\nexport {\n normalizeRemoteUrl,\n resolveRedirectUrl,\n parseAllowedHosts,\n isPrivateOrInternalHost,\n stripBrackets,\n stripTrailingDot,\n RemoteUrlRejectedError,\n MAX_REMOTE_URL_LENGTH,\n MAX_REDIRECTS,\n BLOCKED_HOST_SUFFIXES,\n BLOCKED_HOSTNAMES,\n};\nexport type { UrlPolicyOptions };\n"]}
1
+ {"version":3,"file":"RemoteUrlPolicy.js","sourceRoot":"","sources":["../src/RemoteUrlPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Egress policy and SSRF host predicates for user-supplied URLs.\n *\n * ## Provenance\n *\n * Merged from Durable-DAV's `packages/shared/src/net/RemoteUrlPolicy.ts` and the\n * `SsrfHosts.ts` of Durable-DAV-Router, Edge-Git and Edge-Sonic. The two were the\n * same problem solved twice: Durable-DAV's `RemoteUrlPolicy` is the full\n * validate-and-canonicalize policy (HTTPS-only, public-address-only, operator\n * allow-list, redirect re-validation); the Router/Edge-Git/Edge-Sonic\n * `SsrfHosts` are the lower-level \"is this host private?\" predicate the policy in\n * each repo leaned on. Canonical name is **`RemoteUrlPolicy`** (Durable-DAV's);\n * the host predicate it converges onto is exported as **`isPrivateOrInternalHost`**\n * (the common `isSsrfBlockedHost`-style composite from SsrfHosts).\n *\n * Scheduled replication / webhook delivery / git import turns a caller into\n * someone who can name a host this Worker connects to. Without a policy that is\n * server-side request forgery against Cloudflare's own egress: the metadata\n * service, the loopback interface, and anything reachable on a private network the\n * Worker happens to share.\n *\n * ## Design decisions\n *\n * - **The octet-accurate IP checks win over SsrfHosts' naive string prefixes.**\n * SsrfHosts' `isBlockedIpv6Host` matched `startsWith('fc')`/`'fd')`, which also\n * matches a *hostname* like `fcb.example.com`; Durable-DAV's `isBlockedIpv6` /\n * `isBlockedIpv4` parse the address (and decode IPv4-mapped IPv6) instead. The\n * kit keeps the rigorous form for both the URL policy and the exported\n * predicate, so there is one correct answer rather than two that drift.\n * - **`isPrivateOrInternalHost` is strictly safer than either source.** It folds\n * in SsrfHosts' raw-hostname handling (bare-integer/octal/hex/`0x`-dotted forms,\n * trailing-dot and bracket stripping, localhost names and `.local`/`.internal`/\n * `.home.arpa` suffixes) that Durable-DAV's URL-path checks could skip because\n * `new URL` had already canonicalized the host. A public IPv6 literal is allowed\n * exactly as `normalizeRemoteUrl` allows it; every private/loopback/unique-local/\n * link-local/zone/mapped form is refused.\n *\n * ## Known gap, stated rather than hidden\n *\n * DNS rebinding is not covered. A host that resolves to a public address at\n * validation time and a private one when the connection is made defeats every\n * check here. Closing it needs a resolution-aware fetch, which the platform does\n * not expose to a Worker. The upheld rule for such a deployment is to pin an\n * origin allow-list.\n */\n\nconst MAX_REMOTE_URL_LENGTH = 2048;\nconst MAX_REDIRECTS = 3;\n\n/**\n * Host suffixes that never name a real internet peer.\n */\nconst BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.home.arpa'] as const;\n\n/**\n * Hostnames that resolve inward by definition.\n */\nconst BLOCKED_HOSTNAMES = new Set(['localhost', 'localhost.localdomain', 'ip6-localhost', 'ip6-loopback']);\n\ntype UrlPolicyOptions = {\n /**\n * Hostnames (exact, or `.suffix` form) permitted regardless of the address\n * rules. Only ever populated from operator configuration, never from the\n * request body.\n */\n allowedHosts?: readonly string[];\n};\n\nclass RemoteUrlRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteUrlRejectedError';\n }\n}\n\nfunction isIpv4Literal(host: string): boolean {\n return /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host);\n}\n\nfunction ipv4Octets(host: string): number[] | null {\n const parts = host.split('.');\n if (parts.length !== 4) return null;\n const octets: number[] = [];\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const value = Number(part);\n if (value > 255) return null;\n octets.push(value);\n }\n return octets;\n}\n\n/**\n * Is this IPv4 literal in a range a Worker must never reach?\n *\n * `0.0.0.0/8` and `169.254.0.0/16` matter as much as RFC 1918: the first is\n * \"this host\" on Linux, the second is the cloud metadata endpoint.\n */\nfunction isBlockedIpv4(octets: number[]): boolean {\n const [a = 0, b = 0] = octets;\n if ([0, 10, 127].includes(a)) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n return false;\n}\n\n/**\n * Decode an IPv4-mapped IPv6 address back into its four octets, or null.\n *\n * Both spellings have to be handled. `new URL` canonicalizes\n * `https://[::ffff:10.0.0.1]/` to the hex form `[::ffff:a00:1]`, so a check\n * written only against the dotted text never fires — and that address connects to\n * a private network.\n */\nfunction mappedIpv4Octets(literal: string): number[] | null {\n const dotted = /^::ffff:\\d{1,3}(?:\\.\\d{1,3}){3}$/.exec(literal);\n const dottedText = literal.slice('::ffff:'.length);\n if (dotted !== null) return ipv4Octets(dottedText);\n const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(literal);\n if (!hex?.[1] || !hex[2]) return null;\n const high = Number.parseInt(hex[1], 16);\n const low = Number.parseInt(hex[2], 16);\n return !Number.isFinite(high) || !Number.isFinite(low) ? null : [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff];\n}\n\n/**\n * Is this IPv6 literal private, loopback, or an IPv4 address in disguise? `literal`\n * is the **unbracketed** inner form (e.g. `::1`, `fe80::1`).\n *\n * The last case is the one that matters: `::ffff:127.0.0.1` is a perfectly valid\n * URL and connects to loopback, so the embedded IPv4 must be re-checked rather\n * than the literal waved through because it \"is\" IPv6.\n */\nfunction isBlockedIpv6(literal: string): boolean {\n const lower = literal.toLowerCase();\n if (lower === '::' || lower === '::1') return true;\n // Zone ids (`fe80::1%eth0`) name a local interface directly.\n if (lower.includes('%')) return true;\n const mapped = mappedIpv4Octets(lower);\n if (mapped !== null) return isBlockedIpv4(mapped);\n const head = lower.split(':', 1)[0] ?? '';\n if (head === '') return false;\n const group = Number.parseInt(head.padStart(4, '0').slice(0, 4), 16);\n if (!Number.isFinite(group)) return false;\n // fc00::/7 unique-local, fe80::/10 link-local.\n return (group & 0xfe_00) === 0xfc_00 || (group & 0xff_c0) === 0xfe_80;\n}\n\n/**\n * Does this host name a numeric address in a non-canonical spelling? The strict\n * superset of Durable-DAV's `looksLikeObfuscatedNumericHost` and SsrfHosts'\n * `isEncodedNumericHost`, for the raw-hostname predicate.\n *\n * `2130706433`, `0177.0.0.1`, `0x7f000001` and `0x7f.1` all name `127.0.0.1`.\n * Through `new URL` these arrive already canonicalized (so `isBlockedIpv4` catches\n * them); a hostname handed to `isPrivateOrInternalHost` need not have gone through\n * `URL`, so a bare integer/octal/hex form is caught here directly. A legitimate\n * internet hostname is never all-digits, so over-blocking is not a risk.\n */\nfunction looksLikeObfuscatedNumericHost(host: string): boolean {\n if (isIpv4Literal(host) || host.startsWith('[') || !/^[0-9a-fx.]+$/i.test(host)) return false;\n return (\n /^0x[0-9a-f]+$/i.test(host) ||\n /^\\d+$/.test(host) ||\n /^0[0-7]+(?:\\.0[0-7]+)*$/.test(host) ||\n // An all-digits-and-dots host that is not a canonical dotted-quad — e.g.\n // `0177.0.0.1` or `0200.0250.0001.0001` — is an obfuscated numeric address.\n /^[\\d.]+$/.test(host) ||\n host.split('.').some((group) => /^0x[0-9a-f]+$/i.test(group))\n );\n}\n\nfunction hostIsAllowed(host: string, allowedHosts: readonly string[]): boolean {\n const lower = host.toLowerCase();\n return allowedHosts.some((entry) => {\n const candidate = entry.trim().toLowerCase();\n if (candidate === '') return false;\n return candidate.startsWith('.') ? lower.endsWith(candidate) : lower === candidate;\n });\n}\n\n/**\n * Strip one enclosing `[`…`]` pair (an IPv6 literal's URL brackets).\n */\nfunction stripBrackets(host: string): string {\n const h = host.trim();\n return h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h;\n}\n\n/**\n * Strip a bracketed wrapper and every trailing root-label dot.\n *\n * A fully-qualified name ends in a root label (`example.com.`) and `new URL` keeps\n * it; RFC 4918 §9.1.2 clients vary on sending it, so the canonical form is compared.\n */\nfunction stripTrailingDot(host: string): string {\n const unbracketed = stripBrackets(host);\n let end = unbracketed.length;\n while (end > 0 && unbracketed.charAt(end - 1) === '.') end -= 1;\n return unbracketed.slice(0, end);\n}\n\n/**\n * Host names that mean \"this machine or a local suffix\", not an internet peer.\n */\nfunction isLocalhostName(host: string): boolean {\n return BLOCKED_HOSTNAMES.has(host) || host.endsWith('.localhost');\n}\n\n/**\n * True when a URL host resolves to this machine or a private network.\n *\n * Intended for user-supplied URLs that the server will subsequently fetch. Such a\n * URL turns the process into a proxy into its own network: without this, a\n * user-supplied `baseUrl` can point at cloud metadata, a database host, or a\n * loopback admin port, and the response is readable by the caller.\n *\n * Encoded-numeric hosts are included because `http://2130706433/` and\n * `http://0177.0.0.1/` are ordinary-looking strings that name `127.0.0.1` after\n * parsing.\n *\n * A *strict, safer* predicate: an empty host, any private/loopback/link-local/\n * unique-local/mapped form, any localhost/suffix name, any obfuscated numeric\n * form, and any `:`-bearing host that is not a public IPv6 literal all return\n * true. It is a *syntactic* check — a public hostname can still resolve to a\n * private address (DNS rebinding) — so a hard-guarantee deployment additionally\n * pins an allow-list (see the file header).\n */\nfunction isPrivateOrInternalHost(hostname: string): boolean {\n const host = stripTrailingDot(hostname).toLowerCase();\n if (host.length === 0) return true;\n if (isLocalhostName(host)) return true;\n if (BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))) return true;\n\n if (isIpv4Literal(host)) {\n const octets = ipv4Octets(host);\n return octets === null || isBlockedIpv4(octets);\n }\n if (host.includes(':')) return isBlockedIpv6(host);\n if (looksLikeObfuscatedNumericHost(host)) return true;\n return false;\n}\n\n/**\n * Validate and canonicalise a remote URL.\n *\n * Throws `RemoteUrlRejectedError` with a message meant for the owner: every\n * rejection here is a configuration mistake or an attack, and saying which one\n * is what makes the difference between a fixable setup error and a support\n * ticket. Query and fragment are dropped — a WebDAV base URL has no use for\n * either, and keeping them would let `?a=b` smuggle a second meaning past the\n * path checks the sync engine does.\n */\nfunction normalizeRemoteUrl(raw: unknown, options: UrlPolicyOptions = {}): string {\n if (typeof raw !== 'string') throw new RemoteUrlRejectedError('remoteUrl must be a string');\n const trimmed = raw.trim();\n if (trimmed === '') throw new RemoteUrlRejectedError('remoteUrl is required');\n if (trimmed.length > MAX_REMOTE_URL_LENGTH)\n throw new RemoteUrlRejectedError(`remoteUrl must be at most ${MAX_REMOTE_URL_LENGTH} characters`);\n\n let url: URL;\n try {\n url = new URL(trimmed);\n } catch {\n throw new RemoteUrlRejectedError('remoteUrl is not a valid absolute URL');\n }\n\n if (url.protocol !== 'https:') throw new RemoteUrlRejectedError('remoteUrl must use https');\n if (url.username !== '' || url.password !== '')\n throw new RemoteUrlRejectedError('remoteUrl must not embed credentials; use the auth fields instead');\n if (url.port !== '' && url.port !== '443') throw new RemoteUrlRejectedError('remoteUrl must use the default https port');\n\n const host = url.hostname.toLowerCase();\n if (host === '') throw new RemoteUrlRejectedError('remoteUrl must include a host');\n if (hostIsAllowed(host, options.allowedHosts ?? [])) {\n url.search = '';\n url.hash = '';\n return url.href;\n }\n\n if (BLOCKED_HOSTNAMES.has(host)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not routable`);\n for (const suffix of BLOCKED_HOST_SUFFIXES) {\n if (host.endsWith(suffix)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public hostname`);\n }\n if (looksLikeObfuscatedNumericHost(host))\n throw new RemoteUrlRejectedError(`remoteUrl host ${host} is a numeric address in a non-canonical form`);\n\n if (isIpv4Literal(host)) {\n const octets = ipv4Octets(host);\n if (octets === null || isBlockedIpv4(octets)) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);\n } else if (host.startsWith('[')) {\n if (isBlockedIpv6(host.slice(1, -1))) throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a public address`);\n } else if (!host.includes('.')) {\n // A single-label name can only be resolved through a local search domain.\n throw new RemoteUrlRejectedError(`remoteUrl host ${host} is not a fully qualified hostname`);\n }\n\n url.search = '';\n url.hash = '';\n return url.href;\n}\n\n/**\n * Re-validate the target of a redirect.\n *\n * Called for every hop rather than once at the start. Validating the configured\n * URL and then following `Location` unchecked is a classic TOCTOU: the first\n * hop is public, the second is `169.254.169.254`, and the policy has already\n * returned by then.\n */\nfunction resolveRedirectUrl(baseUrl: string, location: string, options: UrlPolicyOptions = {}): string {\n let next: URL;\n try {\n next = new URL(location, baseUrl);\n } catch {\n throw new RemoteUrlRejectedError('redirect target is not a valid URL');\n }\n if (next.protocol !== 'https:') throw new RemoteUrlRejectedError('redirect target must use https');\n // Same host: no need to re-run the whole policy, and a redirect back to the\n // origin's own host is the overwhelmingly common case.\n if (next.host.toLowerCase() === new URL(baseUrl).host.toLowerCase()) {\n next.search = '';\n next.hash = '';\n return next.href;\n }\n return normalizeRemoteUrl(next.href, options);\n}\n\nfunction parseAllowedHosts(raw: unknown): string[] {\n return typeof raw !== 'string' || raw.trim() === ''\n ? []\n : raw\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry !== '');\n}\n\nexport {\n normalizeRemoteUrl,\n resolveRedirectUrl,\n parseAllowedHosts,\n isPrivateOrInternalHost,\n stripBrackets,\n stripTrailingDot,\n RemoteUrlRejectedError,\n MAX_REMOTE_URL_LENGTH,\n MAX_REDIRECTS,\n BLOCKED_HOST_SUFFIXES,\n BLOCKED_HOSTNAMES,\n};\nexport type { UrlPolicyOptions };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexezuge/shared",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {