@fgv/ts-extras 5.1.0-46 → 5.1.0-48

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.
Files changed (106) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +90 -0
  3. package/dist/index.browser.js +3 -1
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.js +2 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/packlets/safer-fetch/addressClassification.js +438 -0
  8. package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
  9. package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
  10. package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
  11. package/dist/packlets/safer-fetch/contentType.js +120 -0
  12. package/dist/packlets/safer-fetch/contentType.js.map +1 -0
  13. package/dist/packlets/safer-fetch/deadline.js +254 -0
  14. package/dist/packlets/safer-fetch/deadline.js.map +1 -0
  15. package/dist/packlets/safer-fetch/defaults.js +131 -0
  16. package/dist/packlets/safer-fetch/defaults.js.map +1 -0
  17. package/dist/packlets/safer-fetch/failureReason.js +21 -0
  18. package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
  19. package/dist/packlets/safer-fetch/guards.js +148 -0
  20. package/dist/packlets/safer-fetch/guards.js.map +1 -0
  21. package/dist/packlets/safer-fetch/index.browser.js +58 -0
  22. package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
  23. package/dist/packlets/safer-fetch/index.js +52 -0
  24. package/dist/packlets/safer-fetch/index.js.map +1 -0
  25. package/dist/packlets/safer-fetch/model.js +21 -0
  26. package/dist/packlets/safer-fetch/model.js.map +1 -0
  27. package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
  28. package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  29. package/dist/packlets/safer-fetch/redirect.js +93 -0
  30. package/dist/packlets/safer-fetch/redirect.js.map +1 -0
  31. package/dist/packlets/safer-fetch/retry.js +188 -0
  32. package/dist/packlets/safer-fetch/retry.js.map +1 -0
  33. package/dist/packlets/safer-fetch/saferFetch.js +755 -0
  34. package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
  35. package/dist/packlets/safer-fetch/transport.js +50 -0
  36. package/dist/packlets/safer-fetch/transport.js.map +1 -0
  37. package/dist/ts-extras.d.ts +1337 -0
  38. package/lib/index.browser.d.ts +2 -1
  39. package/lib/index.browser.d.ts.map +1 -1
  40. package/lib/index.browser.js +4 -1
  41. package/lib/index.browser.js.map +1 -1
  42. package/lib/index.d.ts +2 -1
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js +3 -1
  45. package/lib/index.js.map +1 -1
  46. package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
  47. package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
  48. package/lib/packlets/safer-fetch/addressClassification.js +441 -0
  49. package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
  50. package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
  51. package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
  52. package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
  53. package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
  54. package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
  55. package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
  56. package/lib/packlets/safer-fetch/contentType.js +127 -0
  57. package/lib/packlets/safer-fetch/contentType.js.map +1 -0
  58. package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
  59. package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
  60. package/lib/packlets/safer-fetch/deadline.js +258 -0
  61. package/lib/packlets/safer-fetch/deadline.js.map +1 -0
  62. package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
  63. package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
  64. package/lib/packlets/safer-fetch/defaults.js +134 -0
  65. package/lib/packlets/safer-fetch/defaults.js.map +1 -0
  66. package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
  67. package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
  68. package/lib/packlets/safer-fetch/failureReason.js +22 -0
  69. package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
  70. package/lib/packlets/safer-fetch/guards.d.ts +62 -0
  71. package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
  72. package/lib/packlets/safer-fetch/guards.js +153 -0
  73. package/lib/packlets/safer-fetch/guards.js.map +1 -0
  74. package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
  75. package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
  76. package/lib/packlets/safer-fetch/index.browser.js +82 -0
  77. package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
  78. package/lib/packlets/safer-fetch/index.d.ts +27 -0
  79. package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
  80. package/lib/packlets/safer-fetch/index.js +78 -0
  81. package/lib/packlets/safer-fetch/index.js.map +1 -0
  82. package/lib/packlets/safer-fetch/model.d.ts +373 -0
  83. package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
  84. package/lib/packlets/safer-fetch/model.js +22 -0
  85. package/lib/packlets/safer-fetch/model.js.map +1 -0
  86. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
  87. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
  88. package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
  89. package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  90. package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
  91. package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
  92. package/lib/packlets/safer-fetch/redirect.js +98 -0
  93. package/lib/packlets/safer-fetch/redirect.js.map +1 -0
  94. package/lib/packlets/safer-fetch/retry.d.ts +162 -0
  95. package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
  96. package/lib/packlets/safer-fetch/retry.js +197 -0
  97. package/lib/packlets/safer-fetch/retry.js.map +1 -0
  98. package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
  99. package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
  100. package/lib/packlets/safer-fetch/saferFetch.js +760 -0
  101. package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
  102. package/lib/packlets/safer-fetch/transport.d.ts +18 -0
  103. package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
  104. package/lib/packlets/safer-fetch/transport.js +53 -0
  105. package/lib/packlets/safer-fetch/transport.js.map +1 -0
  106. package/package.json +17 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addressClassification.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/addressClassification.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n// Address classification is prefix arithmetic: every range test is a mask and a\n// compare, and every IPv6 group/byte conversion is a shift. Spelling those as\n// arithmetic instead would be strictly harder to audit against the RFCs, which\n// is the opposite of what this file needs. The repo already takes per-site\n// disables for the same reason in `crypto-utils/keystore` and `ts-random`.\n/* eslint-disable no-bitwise */\n\nimport { type Result, fail, succeed } from '@fgv/ts-utils';\n\n/**\n * The address family of a parsed IP address literal.\n * @public\n */\nexport type AddressFamily = 'ipv4' | 'ipv6';\n\n/**\n * The security-relevant classification of an IP address.\n *\n * `'public'` is the only classification that describes an address which is\n * globally routable on the public internet; every other value names a\n * special-purpose range that a server-side fetch reaching untrusted input\n * should not be able to reach by default.\n *\n * @public\n */\nexport type AddressClassification =\n /** Globally routable unicast — the only classification a default guard permits. */\n | 'public'\n /** `0.0.0.0/8` and `::` — \"this host on this network\"; routes to localhost on Linux. */\n | 'unspecified'\n /** `127.0.0.0/8` and `::1`. */\n | 'loopback'\n /**\n * `169.254.0.0/16` and `fe80::/10`. The IPv4 range contains the cloud\n * instance-metadata endpoint (`169.254.169.254`) and is the single\n * highest-value SSRF target.\n */\n | 'link-local'\n /** RFC 1918 — `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. */\n | 'private'\n /** RFC 4193 IPv6 unique-local — `fc00::/7`. */\n | 'unique-local'\n /** RFC 6598 carrier-grade NAT — `100.64.0.0/10`. Frequently carrier or container internal. */\n | 'carrier-grade-nat'\n /** RFC 2544 benchmarking — `198.18.0.0/15`. */\n | 'benchmarking'\n /** Documentation ranges — `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32`. */\n | 'documentation'\n /** IETF protocol assignments — `192.0.0.0/24` and `2001::/23`. */\n | 'protocol-assignment'\n /** `224.0.0.0/4` and `ff00::/8`. */\n | 'multicast'\n /** `255.255.255.255/32`. */\n | 'broadcast'\n /** Every other special-purpose or future-use range (e.g. `240.0.0.0/4`, `fec0::/10`). */\n | 'reserved';\n\n/**\n * The way an IPv6 address carried the IPv4 address whose classification was used.\n *\n * Each of these is a documented SSRF bypass: an address such as\n * `::ffff:169.254.169.254` is an IPv6 address by family, but the address the\n * connection actually reaches is the embedded IPv4 one, so the embedded\n * address is what must be classified.\n *\n * @public\n */\nexport type Ipv4EmbeddingKind =\n /** `::ffff:0:0/96` — the IPv4-mapped IPv6 form. */\n | 'ipv4-mapped'\n /** `::/96` — the deprecated (RFC 4291) IPv4-compatible form. */\n | 'ipv4-compatible'\n /** `64:ff9b::/96` — the RFC 6052 well-known NAT64 prefix. */\n | 'nat64'\n /** `2002::/16` — the RFC 3056 6to4 form, whose IPv4 lives in bits 16..47. */\n | '6to4';\n\n/**\n * The IPv4 address an IPv6 address embedded, and the way it embedded it.\n * @public\n */\nexport interface IEmbeddedIpv4 {\n /** The encoding that carried the IPv4 address. */\n readonly kind: Ipv4EmbeddingKind;\n /** The dotted-quad form of the embedded IPv4 address. */\n readonly address: string;\n}\n\n/**\n * The result of classifying a single IP address literal.\n * @public\n */\nexport interface IClassifiedAddress {\n /** The address exactly as supplied to {@link classifyAddress}. */\n readonly address: string;\n /**\n * A canonical textual form of the address: dotted-quad for IPv4, and\n * lowercase RFC 5952 hex-group form (with `::` run compression) for IPv6.\n * Note that IPv6 addresses embedding an IPv4 address canonicalize to their\n * hex-group form (`::ffff:a9fe:a9fe`), not to the dotted-quad mixed form;\n * `embeddedIpv4` carries the readable IPv4 value.\n */\n readonly canonical: string;\n /** The address family of the literal as supplied — never the embedded family. */\n readonly family: AddressFamily;\n /**\n * The classification used for policy decisions. For an IPv6 address that\n * embeds an IPv4 address, this is the classification of the *embedded* IPv4\n * address.\n */\n readonly classification: AddressClassification;\n /**\n * Present exactly when `classification` was derived from an IPv4 address\n * embedded in this IPv6 address, and absent otherwise.\n */\n readonly embeddedIpv4?: IEmbeddedIpv4;\n}\n\ninterface IIpv4Range {\n readonly network: number;\n readonly prefixLength: number;\n readonly classification: AddressClassification;\n}\n\ninterface IIpv6Range {\n /** Leading bytes of the prefix; length is `ceil(prefixLength / 8)`. */\n readonly prefix: ReadonlyArray<number>;\n readonly prefixLength: number;\n readonly classification: AddressClassification;\n}\n\n/**\n * IPv4 special-purpose ranges, most specific first. The broadcast address is\n * listed ahead of `240.0.0.0/4` because it falls inside it.\n */\nconst IPV4_RANGES: ReadonlyArray<IIpv4Range> = [\n { network: 0xffffffff, prefixLength: 32, classification: 'broadcast' },\n { network: 0x00000000, prefixLength: 8, classification: 'unspecified' },\n { network: 0x0a000000, prefixLength: 8, classification: 'private' },\n { network: 0x64400000, prefixLength: 10, classification: 'carrier-grade-nat' },\n { network: 0x7f000000, prefixLength: 8, classification: 'loopback' },\n { network: 0xa9fe0000, prefixLength: 16, classification: 'link-local' },\n { network: 0xac100000, prefixLength: 12, classification: 'private' },\n { network: 0xc0000000, prefixLength: 24, classification: 'protocol-assignment' },\n { network: 0xc0000200, prefixLength: 24, classification: 'documentation' },\n { network: 0xc0586300, prefixLength: 24, classification: 'reserved' },\n { network: 0xc0a80000, prefixLength: 16, classification: 'private' },\n { network: 0xc6120000, prefixLength: 15, classification: 'benchmarking' },\n { network: 0xc6336400, prefixLength: 24, classification: 'documentation' },\n { network: 0xcb007100, prefixLength: 24, classification: 'documentation' },\n { network: 0xe0000000, prefixLength: 4, classification: 'multicast' },\n { network: 0xf0000000, prefixLength: 4, classification: 'reserved' }\n];\n\n/**\n * IPv6 special-purpose ranges, most specific first. Ranges whose addresses\n * embed an IPv4 address are handled separately, before this table is consulted,\n * and anything outside global unicast (`2000::/3`) that matches nothing here is\n * classified `'reserved'` — so this table only needs to name the ranges that\n * deserve a more specific classification than that fallback gives them.\n */\nconst IPV6_RANGES: ReadonlyArray<IIpv6Range> = [\n { prefix: [0x20, 0x01, 0x0d, 0xb8], prefixLength: 32, classification: 'documentation' },\n { prefix: [0x20, 0x01, 0x00], prefixLength: 23, classification: 'protocol-assignment' },\n { prefix: [0x3f, 0xff, 0x00], prefixLength: 20, classification: 'documentation' },\n { prefix: [0xfc], prefixLength: 7, classification: 'unique-local' },\n { prefix: [0xfe, 0x80], prefixLength: 10, classification: 'link-local' },\n { prefix: [0xff], prefixLength: 8, classification: 'multicast' }\n];\n\n/** The first three bits of a global unicast IPv6 address (`2000::/3`). */\nconst IPV6_GLOBAL_UNICAST_MASK: number = 0xe0;\nconst IPV6_GLOBAL_UNICAST_VALUE: number = 0x20;\n\n/**\n * Parses one component of an IPv4 literal using `inet_aton` conventions: a\n * `0x`/`0X` prefix selects hexadecimal, an otherwise-leading `0` selects octal,\n * and anything else is decimal.\n *\n * This is deliberately permissive about *radix* because the WHATWG URL parser\n * is: `http://0177.0.0.1/` and `http://2130706433/` are both `127.0.0.1`, and a\n * classifier that only understands dotted decimal is bypassed by either.\n */\nfunction parseIpv4Component(text: string): number | undefined {\n if (text.length === 0) {\n return undefined;\n }\n\n let radix: number = 10;\n let digits: string = text;\n if (text.length > 1 && (text[1] === 'x' || text[1] === 'X') && text[0] === '0') {\n radix = 16;\n digits = text.slice(2);\n } else if (text.length > 1 && text[0] === '0') {\n radix = 8;\n digits = text.slice(1);\n }\n\n // The WHATWG IPv4 number parser returns 0 when stripping the radix prefix\n // leaves nothing behind, so a bare `0x` is zero: `127.0x.1` is `127.0.0.1`.\n // Rejecting it here would leave a live encoding bypass.\n if (digits.length === 0) {\n return 0;\n }\n\n const allowed: RegExp = radix === 16 ? /^[0-9a-fA-F]+$/ : radix === 8 ? /^[0-7]+$/ : /^[0-9]+$/;\n if (!allowed.test(digits)) {\n return undefined;\n }\n\n const value: number = Number.parseInt(digits, radix);\n return Number.isSafeInteger(value) ? value : undefined;\n}\n\n/**\n * Parses an IPv4 literal in any of the forms the platform URL parser accepts\n * (`a.b.c.d`, `a.b.c`, `a.b`, `a`), returning the address as an unsigned 32-bit\n * value. The final component absorbs all remaining bytes, so `127.1` is\n * `127.0.0.1` and `2130706433` is also `127.0.0.1`.\n */\nfunction parseIpv4(text: string): number | undefined {\n const parts: string[] = text.split('.');\n if (parts.length > 4) {\n return undefined;\n }\n\n const values: number[] = [];\n for (const part of parts) {\n const value: number | undefined = parseIpv4Component(part);\n if (value === undefined) {\n return undefined;\n }\n values.push(value);\n }\n\n const leading: number = values.length - 1;\n for (let i: number = 0; i < leading; i++) {\n if (values[i] > 0xff) {\n return undefined;\n }\n }\n if (values[leading] > Math.pow(256, 4 - leading) - 1) {\n return undefined;\n }\n\n let address: number = values[leading];\n for (let i: number = 0; i < leading; i++) {\n address += values[i] * Math.pow(256, 3 - i);\n }\n return address >>> 0;\n}\n\n/**\n * Parses a strict dotted-quad IPv4 literal — exactly four decimal components,\n * each `0..255`, with no octal or hexadecimal forms. This is the grammar RFC\n * 4291 permits for the IPv4 tail of an IPv6 literal, and it is deliberately\n * stricter than {@link parseIpv4}: an unparseable literal fails closed.\n */\nfunction parseStrictDottedQuad(text: string): number | undefined {\n const parts: string[] = text.split('.');\n if (parts.length !== 4) {\n return undefined;\n }\n\n let address: number = 0;\n for (const part of parts) {\n if (!/^(0|[1-9][0-9]{0,2})$/.test(part)) {\n return undefined;\n }\n const value: number = Number.parseInt(part, 10);\n if (value > 0xff) {\n return undefined;\n }\n address = address * 256 + value;\n }\n return address >>> 0;\n}\n\n/**\n * Parses a colon-separated run of IPv6 groups, allowing a trailing embedded\n * dotted-quad IPv4 literal which expands to the final two groups.\n *\n * @param allowEmbeddedIpv4 - whether this run may end in a dotted-quad IPv4\n * literal. Only the run that ends the whole address may, so the run *before* a\n * `::` never does: `1.2.3.4::` is malformed, not `102:304::`.\n */\nfunction parseIpv6Groups(text: string, allowEmbeddedIpv4: boolean): number[] | undefined {\n if (text.length === 0) {\n return [];\n }\n\n const parts: string[] = text.split(':');\n const groups: number[] = [];\n for (let i: number = 0; i < parts.length; i++) {\n const part: string = parts[i];\n if (part.indexOf('.') >= 0) {\n if (!allowEmbeddedIpv4 || i !== parts.length - 1) {\n return undefined;\n }\n const embedded: number | undefined = parseStrictDottedQuad(part);\n if (embedded === undefined) {\n return undefined;\n }\n groups.push((embedded >>> 16) & 0xffff, embedded & 0xffff);\n continue;\n }\n if (!/^[0-9a-fA-F]{1,4}$/.test(part)) {\n return undefined;\n }\n groups.push(Number.parseInt(part, 16));\n }\n return groups;\n}\n\n/**\n * Parses an IPv6 literal into its 16 constituent bytes.\n */\nfunction parseIpv6(text: string): Uint8Array | undefined {\n const doubleColon: number = text.indexOf('::');\n let groups: number[] | undefined;\n\n if (doubleColon >= 0) {\n if (text.indexOf('::', doubleColon + 1) >= 0) {\n return undefined;\n }\n const head: number[] | undefined = parseIpv6Groups(text.slice(0, doubleColon), false);\n const tail: number[] | undefined = parseIpv6Groups(text.slice(doubleColon + 2), true);\n if (head === undefined || tail === undefined) {\n return undefined;\n }\n const elided: number = 8 - head.length - tail.length;\n if (elided < 1) {\n return undefined;\n }\n groups = head.concat(new Array<number>(elided).fill(0), tail);\n } else {\n groups = parseIpv6Groups(text, true);\n if (groups === undefined || groups.length !== 8) {\n return undefined;\n }\n }\n\n const bytes: Uint8Array = new Uint8Array(16);\n for (let i: number = 0; i < 8; i++) {\n bytes[i * 2] = (groups[i] >>> 8) & 0xff;\n bytes[i * 2 + 1] = groups[i] & 0xff;\n }\n return bytes;\n}\n\nfunction ipv4ToString(address: number): string {\n return [(address >>> 24) & 0xff, (address >>> 16) & 0xff, (address >>> 8) & 0xff, address & 0xff].join('.');\n}\n\n/**\n * Renders the RFC 5952 canonical text form of an IPv6 address: lowercase, no\n * leading zeros in a group, and the leftmost longest run of two or more\n * all-zero groups replaced by `::`.\n */\nfunction ipv6ToString(bytes: Uint8Array): string {\n const groups: number[] = [];\n for (let i: number = 0; i < 8; i++) {\n groups.push((bytes[i * 2] << 8) | bytes[i * 2 + 1]);\n }\n\n let bestStart: number = -1;\n let bestLength: number = 0;\n let runStart: number = -1;\n let runLength: number = 0;\n for (let i: number = 0; i < 8; i++) {\n if (groups[i] === 0) {\n if (runStart < 0) {\n runStart = i;\n runLength = 0;\n }\n runLength++;\n if (runLength > bestLength) {\n bestStart = runStart;\n bestLength = runLength;\n }\n } else {\n runStart = -1;\n runLength = 0;\n }\n }\n\n const rendered: string[] = groups.map((g) => g.toString(16));\n if (bestLength < 2) {\n return rendered.join(':');\n }\n const head: string = rendered.slice(0, bestStart).join(':');\n const tail: string = rendered.slice(bestStart + bestLength).join(':');\n return `${head}::${tail}`;\n}\n\nfunction classifyIpv4Value(address: number): AddressClassification {\n for (const range of IPV4_RANGES) {\n const mask: number = (0xffffffff << (32 - range.prefixLength)) >>> 0;\n if ((address & mask) >>> 0 === range.network) {\n return range.classification;\n }\n }\n return 'public';\n}\n\nfunction matchesIpv6Range(bytes: Uint8Array, range: IIpv6Range): boolean {\n const fullBytes: number = range.prefixLength >>> 3;\n for (let i: number = 0; i < fullBytes; i++) {\n if (bytes[i] !== range.prefix[i]) {\n return false;\n }\n }\n const remainingBits: number = range.prefixLength & 7;\n if (remainingBits === 0) {\n return true;\n }\n const mask: number = (0xff << (8 - remainingBits)) & 0xff;\n return (bytes[fullBytes] & mask) === (range.prefix[fullBytes] & mask);\n}\n\nfunction allZero(bytes: Uint8Array, start: number, end: number): boolean {\n for (let i: number = start; i < end; i++) {\n if (bytes[i] !== 0) {\n return false;\n }\n }\n return true;\n}\n\nfunction readIpv4At(bytes: Uint8Array, offset: number): number {\n return (\n ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0\n );\n}\n\ninterface IIpv6Embedding {\n readonly kind: Ipv4EmbeddingKind;\n readonly value: number;\n}\n\n/**\n * Detects the IPv4 address embedded in an IPv6 address, if any. `::` and `::1`\n * are excluded before the IPv4-compatible check so that the unspecified and\n * loopback addresses keep their own classifications.\n */\nfunction detectIpv6Embedding(bytes: Uint8Array): IIpv6Embedding | undefined {\n if (allZero(bytes, 0, 10) && bytes[10] === 0xff && bytes[11] === 0xff) {\n return { kind: 'ipv4-mapped', value: readIpv4At(bytes, 12) };\n }\n if (\n bytes[0] === 0x00 &&\n bytes[1] === 0x64 &&\n bytes[2] === 0xff &&\n bytes[3] === 0x9b &&\n allZero(bytes, 4, 12)\n ) {\n return { kind: 'nat64', value: readIpv4At(bytes, 12) };\n }\n if (bytes[0] === 0x20 && bytes[1] === 0x02) {\n return { kind: '6to4', value: readIpv4At(bytes, 2) };\n }\n if (allZero(bytes, 0, 12)) {\n return { kind: 'ipv4-compatible', value: readIpv4At(bytes, 12) };\n }\n return undefined;\n}\n\nfunction classifyIpv6Bytes(bytes: Uint8Array): IClassifiedAddress {\n const canonical: string = ipv6ToString(bytes);\n\n if (allZero(bytes, 0, 16)) {\n return { address: canonical, canonical, family: 'ipv6', classification: 'unspecified' };\n }\n if (allZero(bytes, 0, 15) && bytes[15] === 0x01) {\n return { address: canonical, canonical, family: 'ipv6', classification: 'loopback' };\n }\n\n const embedding: IIpv6Embedding | undefined = detectIpv6Embedding(bytes);\n if (embedding !== undefined) {\n return {\n address: canonical,\n canonical,\n family: 'ipv6',\n classification: classifyIpv4Value(embedding.value),\n embeddedIpv4: { kind: embedding.kind, address: ipv4ToString(embedding.value) }\n };\n }\n\n for (const range of IPV6_RANGES) {\n if (matchesIpv6Range(bytes, range)) {\n return { address: canonical, canonical, family: 'ipv6', classification: range.classification };\n }\n }\n\n // Only `2000::/3` is assigned as global unicast. Everything else that reached\n // this point is unassigned or reserved for future use, so it is never public.\n const classification: AddressClassification =\n (bytes[0] & IPV6_GLOBAL_UNICAST_MASK) === IPV6_GLOBAL_UNICAST_VALUE ? 'public' : 'reserved';\n return { address: canonical, canonical, family: 'ipv6', classification };\n}\n\n/**\n * Normalizes the textual forms an address can arrive in before parsing:\n * surrounding whitespace and the square brackets a URL `hostname` carries for\n * an IPv6 literal. A zone identifier is *not* stripped here — it is stripped on\n * the IPv6 path only, because `8.8.8.8%something` is not an address with a zone\n * and must fail rather than be read as `8.8.8.8`.\n */\nfunction normalizeAddressText(address: string): string {\n const text: string = address.trim();\n if (text.length > 1 && text[0] === '[' && text[text.length - 1] === ']') {\n return text.slice(1, -1);\n }\n return text;\n}\n\n/** Strips an IPv6 zone identifier (`fe80::1%eth0`). */\nfunction stripIpv6Zone(text: string): string {\n const zone: number = text.indexOf('%');\n return zone >= 0 ? text.slice(0, zone) : text;\n}\n\n/**\n * Classifies a single IP address literal.\n *\n * The function is pure, synchronous, and deterministic — it performs no name\n * resolution and no I/O. Input that is not an IP address literal (a DNS\n * hostname, for instance) fails; callers that accept hostnames should treat a\n * failure here as \"not a literal\" and resolve the name, then classify each\n * resolved address.\n *\n * Accepted forms:\n *\n * - dotted-quad IPv4 (`169.254.169.254`)\n * - the shortened and non-decimal IPv4 forms the WHATWG URL parser accepts\n * (`127.1`, `2130706433`, `0177.0.0.1`, `0x7f.1`)\n * - IPv6, with or without `::` compression, optionally bracketed as a URL\n * `hostname` is (`[::1]`) and optionally carrying a zone id (`fe80::1%eth0`)\n * - IPv6 forms that embed an IPv4 address — IPv4-mapped, IPv4-compatible,\n * NAT64 and 6to4 — which are classified by their embedded IPv4 address\n *\n * A trailing dot is accepted on an IPv4 literal (`127.0.0.1.`), matching URL\n * hostname normalization.\n *\n * **Classify a URL's `hostname`, never the raw URL text.** This function does\n * not apply IDNA/Unicode normalization, and that step is not cosmetic: the\n * platform's URL parser reads `http://127.0.0.1/`, `http://127。0。0。1/` and\n * even `http://⑫7.0.0.1/` as the host `127.0.0.1`, because IDNA normalizes\n * fullwidth digits, the ideographic full stop and circled numbers to their\n * ASCII equivalents. Handed one of those strings directly this function fails —\n * which is fail-closed, but only because the caller is then expected to treat\n * \"not a literal\" as \"resolve it as a name\". Reading `new URL(...).hostname`\n * gets the normalization for free and is the only supported use.\n *\n * @param address - the address literal to classify.\n * @returns `Success` with the {@link IClassifiedAddress | classification},\n * or `Failure` if the supplied text is not a well-formed IP address literal.\n * @public\n */\nexport function classifyAddress(address: string): Result<IClassifiedAddress> {\n const text: string = normalizeAddressText(address);\n if (text.length === 0) {\n return fail(`\"${address}\": not a valid IP address literal (empty)`);\n }\n\n if (text.indexOf(':') >= 0) {\n const bytes: Uint8Array | undefined = parseIpv6(stripIpv6Zone(text));\n if (bytes === undefined) {\n return fail(`\"${address}\": not a valid IPv6 address literal`);\n }\n return succeed({ ...classifyIpv6Bytes(bytes), address });\n }\n\n const v4Text: string = text.length > 1 && text[text.length - 1] === '.' ? text.slice(0, -1) : text;\n const value: number | undefined = parseIpv4(v4Text);\n if (value === undefined) {\n return fail(`\"${address}\": not a valid IP address literal`);\n }\n const canonical: string = ipv4ToString(value);\n return succeed({\n address,\n canonical,\n family: 'ipv4',\n classification: classifyIpv4Value(value)\n });\n}\n"]}
@@ -0,0 +1,119 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { fail, mapResults, succeed } from '@fgv/ts-utils';
21
+ import { classifyAddress } from './addressClassification';
22
+ const PUBLIC_ONLY = new Set(['public']);
23
+ const PUBLIC_OR_LOOPBACK = new Set([
24
+ 'public',
25
+ 'loopback'
26
+ ]);
27
+ function describeAddress(classified) {
28
+ const embedded = classified.embeddedIpv4 !== undefined
29
+ ? ` via ${classified.embeddedIpv4.kind} ${classified.embeddedIpv4.address}`
30
+ : '';
31
+ return `${classified.canonical} is ${classified.classification}${embedded}`;
32
+ }
33
+ function checkOneAddress(policyName, allowed, address) {
34
+ return classifyAddress(address)
35
+ .withErrorFormat((message) => `${policyName}: ${message}`)
36
+ .onSuccess((classified) => allowed.has(classified.classification)
37
+ ? succeed(classified)
38
+ : fail(`${policyName}: ${describeAddress(classified)} and is not allowed`));
39
+ }
40
+ function checkAllAddresses(policyName, allowed, addresses) {
41
+ if (addresses.length === 0) {
42
+ return fail(`${policyName}: no addresses to check - the destination cannot be verified`);
43
+ }
44
+ return mapResults(addresses.map((address) => checkOneAddress(policyName, allowed, address))).onSuccess((classified) => succeed({ policy: policyName, addresses: classified }));
45
+ }
46
+ /**
47
+ * Creates the recommended address policy: every address must be a globally
48
+ * routable public unicast address.
49
+ *
50
+ * Rejected, in both their plain and their IPv6-embedded encodings: loopback,
51
+ * the link-local range that carries the cloud instance-metadata endpoint
52
+ * (`169.254.169.254`), RFC 1918 private ranges, IPv6 unique-local, carrier-grade
53
+ * NAT (`100.64.0.0/10`), the unspecified address, multicast, broadcast,
54
+ * benchmarking, documentation and every other reserved range. The encoding
55
+ * bypasses covered are IPv4-mapped IPv6 (`::ffff:169.254.169.254`),
56
+ * IPv4-compatible IPv6, NAT64 (`64:ff9b::a9fe:a9fe`), 6to4 (`2002:a9fe:a9fe::`)
57
+ * and the shortened / octal / hexadecimal / decimal IPv4 literal forms
58
+ * (`127.1`, `0177.0.0.1`, `0x7f.1`, `2130706433`).
59
+ *
60
+ * **What this does not protect against.** The policy classifies addresses a
61
+ * caller has already resolved; it cannot see the address the connection
62
+ * ultimately uses. A hostile DNS server that answers the resolution with a
63
+ * public address and the connect with a private one is not stopped by this
64
+ * policy — closing that requires connecting to a pinned address. The policy
65
+ * also says nothing about scheme, host, port, redirects or response content.
66
+ *
67
+ * This is the **policy-layer** factory. It is not what a safer-fetch call's
68
+ * `addressGuard` option takes: an entry point takes an `IAddressGuard`, which is
69
+ * the asynchronous, hop-chain-aware, name-resolving half. `blockPrivateNetworks`
70
+ * (Node only) is the guard that resolves a hostname and delegates every resolved
71
+ * address to this policy; reach for that unless you are classifying an address
72
+ * list you already hold.
73
+ *
74
+ * @param options - optional {@link IBlockPrivateNetworksOptions | relaxations}
75
+ * of the default posture.
76
+ * @returns the policy. Construction cannot fail.
77
+ * @public
78
+ */
79
+ export function blockPrivateNetworksPolicy(options) {
80
+ const allowLoopback = (options === null || options === void 0 ? void 0 : options.allowLoopback) === true;
81
+ const name = allowLoopback
82
+ ? 'blockPrivateNetworksPolicy(allowLoopback)'
83
+ : 'blockPrivateNetworksPolicy';
84
+ const allowed = allowLoopback ? PUBLIC_OR_LOOPBACK : PUBLIC_ONLY;
85
+ return {
86
+ name,
87
+ checkAddresses: (addresses) => checkAllAddresses(name, allowed, addresses)
88
+ };
89
+ }
90
+ /**
91
+ * Creates a policy that permits every address, including loopback, link-local
92
+ * and private ones.
93
+ *
94
+ * **This policy provides no protection whatsoever.** It classifies nothing,
95
+ * rejects nothing, and never fails — not even for an empty address list or an
96
+ * address that is not a well-formed literal. It exists so that choosing to go
97
+ * without an address guarantee is a deliberate, named, greppable act at the
98
+ * call site rather than something reachable by omission, and so that tests and
99
+ * genuinely trusted-input paths do not hand-roll something worse.
100
+ *
101
+ * It is the only correct choice in a browser, where neither name resolution nor
102
+ * redirect interposition is available and no address guarantee is possible.
103
+ *
104
+ * @returns the policy.
105
+ * @public
106
+ */
107
+ export function allowAnyAddressPolicy() {
108
+ const name = 'allowAnyAddressPolicy';
109
+ return {
110
+ name,
111
+ // The parameter is deliberately not declared. This policy does not read the
112
+ // addresses, and an unused `_addresses` would both restate that less
113
+ // clearly and cut against the repo's "don't rename unused variables to
114
+ // `_var`" rule. Parameter bivariance makes the zero-arg form a complete
115
+ // implementation of `IAddressPolicy`.
116
+ checkAddresses: () => succeed({ policy: name, addresses: [] })
117
+ };
118
+ }
119
+ //# sourceMappingURL=addressPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addressPolicy.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/addressPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { type Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport {\n type AddressClassification,\n type IClassifiedAddress,\n classifyAddress\n} from './addressClassification';\n\n/**\n * The evidence behind an allowed address-policy decision.\n * @public\n */\nexport interface IAddressCheckVerdict {\n /** The {@link IAddressPolicy.name | name} of the policy that produced the verdict. */\n readonly policy: string;\n /**\n * The classification of every address the policy examined, in the order\n * supplied. Empty for {@link allowAnyAddressPolicy}, which classifies nothing.\n */\n readonly addresses: ReadonlyArray<IClassifiedAddress>;\n}\n\n/**\n * A pure, synchronous decision over a set of IP addresses.\n *\n * A policy performs no name resolution and no I/O: a caller resolves a\n * hostname to its addresses and hands the whole list to\n * {@link IAddressPolicy.checkAddresses}. The list contract is\n * **reject-if-any** — a hostname that resolves to one public and one private\n * address is disallowed, because without connect-time address pinning there is\n * no guarantee which address the connection will use.\n *\n * @public\n */\nexport interface IAddressPolicy {\n /**\n * A stable, greppable identifier for the posture this policy implements.\n * Surfaced in the failure message when an address is disallowed.\n */\n readonly name: string;\n\n /**\n * Decides whether a connection may be made to a destination that resolved to\n * `addresses`.\n *\n * @param addresses - every address the destination resolved to, or the\n * single literal address when the destination was an IP literal.\n * @returns `Success` with the {@link IAddressCheckVerdict | verdict} when\n * *every* address is permitted, `Failure` naming every address that is not.\n * An empty list is a failure for any policy that offers a guarantee: nothing\n * was verified, so nothing may be reached.\n */\n checkAddresses(addresses: ReadonlyArray<string>): Result<IAddressCheckVerdict>;\n}\n\n/**\n * Options shared by {@link blockPrivateNetworksPolicy} and the address guard built over it.\n * @public\n */\nexport interface IBlockPrivateNetworksOptions {\n /**\n * Permits `127.0.0.0/8` and `::1` (and their IPv4-mapped forms). Off by\n * default: the polarity that ships a guard permitting `http://127.0.0.1:6379/`\n * so a local-development convenience keeps working is the wrong one. A\n * caller talking to a local sidecar opts in here, and the opt-in is\n * independently greppable at the call site.\n */\n readonly allowLoopback?: boolean;\n}\n\nconst PUBLIC_ONLY: ReadonlySet<AddressClassification> = new Set<AddressClassification>(['public']);\nconst PUBLIC_OR_LOOPBACK: ReadonlySet<AddressClassification> = new Set<AddressClassification>([\n 'public',\n 'loopback'\n]);\n\nfunction describeAddress(classified: IClassifiedAddress): string {\n const embedded: string =\n classified.embeddedIpv4 !== undefined\n ? ` via ${classified.embeddedIpv4.kind} ${classified.embeddedIpv4.address}`\n : '';\n return `${classified.canonical} is ${classified.classification}${embedded}`;\n}\n\nfunction checkOneAddress(\n policyName: string,\n allowed: ReadonlySet<AddressClassification>,\n address: string\n): Result<IClassifiedAddress> {\n return classifyAddress(address)\n .withErrorFormat((message) => `${policyName}: ${message}`)\n .onSuccess((classified) =>\n allowed.has(classified.classification)\n ? succeed(classified)\n : fail(`${policyName}: ${describeAddress(classified)} and is not allowed`)\n );\n}\n\nfunction checkAllAddresses(\n policyName: string,\n allowed: ReadonlySet<AddressClassification>,\n addresses: ReadonlyArray<string>\n): Result<IAddressCheckVerdict> {\n if (addresses.length === 0) {\n return fail(`${policyName}: no addresses to check - the destination cannot be verified`);\n }\n return mapResults(addresses.map((address) => checkOneAddress(policyName, allowed, address))).onSuccess(\n (classified) => succeed({ policy: policyName, addresses: classified })\n );\n}\n\n/**\n * Creates the recommended address policy: every address must be a globally\n * routable public unicast address.\n *\n * Rejected, in both their plain and their IPv6-embedded encodings: loopback,\n * the link-local range that carries the cloud instance-metadata endpoint\n * (`169.254.169.254`), RFC 1918 private ranges, IPv6 unique-local, carrier-grade\n * NAT (`100.64.0.0/10`), the unspecified address, multicast, broadcast,\n * benchmarking, documentation and every other reserved range. The encoding\n * bypasses covered are IPv4-mapped IPv6 (`::ffff:169.254.169.254`),\n * IPv4-compatible IPv6, NAT64 (`64:ff9b::a9fe:a9fe`), 6to4 (`2002:a9fe:a9fe::`)\n * and the shortened / octal / hexadecimal / decimal IPv4 literal forms\n * (`127.1`, `0177.0.0.1`, `0x7f.1`, `2130706433`).\n *\n * **What this does not protect against.** The policy classifies addresses a\n * caller has already resolved; it cannot see the address the connection\n * ultimately uses. A hostile DNS server that answers the resolution with a\n * public address and the connect with a private one is not stopped by this\n * policy — closing that requires connecting to a pinned address. The policy\n * also says nothing about scheme, host, port, redirects or response content.\n *\n * This is the **policy-layer** factory. It is not what a safer-fetch call's\n * `addressGuard` option takes: an entry point takes an `IAddressGuard`, which is\n * the asynchronous, hop-chain-aware, name-resolving half. `blockPrivateNetworks`\n * (Node only) is the guard that resolves a hostname and delegates every resolved\n * address to this policy; reach for that unless you are classifying an address\n * list you already hold.\n *\n * @param options - optional {@link IBlockPrivateNetworksOptions | relaxations}\n * of the default posture.\n * @returns the policy. Construction cannot fail.\n * @public\n */\nexport function blockPrivateNetworksPolicy(options?: IBlockPrivateNetworksOptions): IAddressPolicy {\n const allowLoopback: boolean = options?.allowLoopback === true;\n const name: string = allowLoopback\n ? 'blockPrivateNetworksPolicy(allowLoopback)'\n : 'blockPrivateNetworksPolicy';\n const allowed: ReadonlySet<AddressClassification> = allowLoopback ? PUBLIC_OR_LOOPBACK : PUBLIC_ONLY;\n return {\n name,\n checkAddresses: (addresses: ReadonlyArray<string>): Result<IAddressCheckVerdict> =>\n checkAllAddresses(name, allowed, addresses)\n };\n}\n\n/**\n * Creates a policy that permits every address, including loopback, link-local\n * and private ones.\n *\n * **This policy provides no protection whatsoever.** It classifies nothing,\n * rejects nothing, and never fails — not even for an empty address list or an\n * address that is not a well-formed literal. It exists so that choosing to go\n * without an address guarantee is a deliberate, named, greppable act at the\n * call site rather than something reachable by omission, and so that tests and\n * genuinely trusted-input paths do not hand-roll something worse.\n *\n * It is the only correct choice in a browser, where neither name resolution nor\n * redirect interposition is available and no address guarantee is possible.\n *\n * @returns the policy.\n * @public\n */\nexport function allowAnyAddressPolicy(): IAddressPolicy {\n const name: string = 'allowAnyAddressPolicy';\n return {\n name,\n // The parameter is deliberately not declared. This policy does not read the\n // addresses, and an unused `_addresses` would both restate that less\n // clearly and cut against the repo's \"don't rename unused variables to\n // `_var`\" rule. Parameter bivariance makes the zero-arg form a complete\n // implementation of `IAddressPolicy`.\n checkAddresses: (): Result<IAddressCheckVerdict> => succeed({ policy: name, addresses: [] })\n };\n}\n"]}
@@ -0,0 +1,120 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { fail, succeed } from '@fgv/ts-utils';
21
+ // RFC 7230 token. Deliberately not a permissive `[^/;]+`: an allowlist entry that is not a
22
+ // well-formed media type should be reported to its author, not quietly compiled into a
23
+ // pattern that never matches anything.
24
+ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
25
+ /**
26
+ * Parses `type/subtype`, ignoring any parameters and surrounding whitespace, and lowercasing
27
+ * both halves.
28
+ *
29
+ * The parse is not as trivial as it looks, which is the argument for owning it once:
30
+ * `text/html; charset=utf-8` must match `text/html`, matching is case-insensitive on both
31
+ * halves, and parameters must be *stripped* rather than string-matched around. Every
32
+ * hand-rolled version gets a different subset of that right, and the failure is silent.
33
+ * @internal
34
+ */
35
+ export function parseMediaType(value) {
36
+ const essence = value.split(';')[0].trim().toLowerCase();
37
+ const slash = essence.indexOf('/');
38
+ if (slash < 0) {
39
+ return fail(`"${value}": not a media type — expected "type/subtype".`);
40
+ }
41
+ const type = essence.slice(0, slash);
42
+ const subtype = essence.slice(slash + 1);
43
+ if (!TOKEN.test(type) && type !== '*') {
44
+ return fail(`"${value}": "${type}" is not a valid media type.`);
45
+ }
46
+ if (!TOKEN.test(subtype) && subtype !== '*') {
47
+ return fail(`"${value}": "${subtype}" is not a valid media subtype.`);
48
+ }
49
+ return succeed({ type, subtype });
50
+ }
51
+ /**
52
+ * Parses an allowlist entry, which may use `*` as a wildcard for the subtype (`text/*`) or for
53
+ * both halves (a wildcard type with a wildcard subtype, which accepts anything). A wildcard
54
+ * type paired with a *concrete* subtype is rejected: it reads as if it means something and
55
+ * does not.
56
+ * @internal
57
+ */
58
+ export function parseMediaTypePattern(value) {
59
+ return parseMediaType(value).onSuccess((parsed) => {
60
+ if (parsed.type === '*' && parsed.subtype !== '*') {
61
+ return fail(`"${value}": a wildcard type requires a wildcard subtype.`);
62
+ }
63
+ return succeed(parsed);
64
+ });
65
+ }
66
+ /**
67
+ * Reports whether a parsed media type matches a parsed allowlist pattern.
68
+ * @internal
69
+ */
70
+ export function mediaTypeMatches(actual, pattern) {
71
+ if (pattern.type === '*') {
72
+ return true;
73
+ }
74
+ if (pattern.type !== actual.type) {
75
+ return false;
76
+ }
77
+ return pattern.subtype === '*' || pattern.subtype === actual.subtype;
78
+ }
79
+ // A parameter value is either a token or a quoted-string; the charset names that matter here
80
+ // (`utf-8`, `iso-8859-1`, …) are tokens either way once the quotes come off.
81
+ const CHARSET_PARAM = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i;
82
+ /**
83
+ * Extracts the `charset` parameter from a `Content-Type` header value, lowercased.
84
+ *
85
+ * Returns `undefined` when the header is absent or carries no `charset`, which the caller
86
+ * reads as "use the default encoding" — not as "any encoding will do".
87
+ * @internal
88
+ */
89
+ export function parseCharset(contentType) {
90
+ var _a;
91
+ if (contentType === undefined) {
92
+ return undefined;
93
+ }
94
+ const match = CHARSET_PARAM.exec(contentType);
95
+ if (match === null) {
96
+ return undefined;
97
+ }
98
+ const value = ((_a = match[1]) !== null && _a !== void 0 ? _a : match[2]).trim().toLowerCase();
99
+ return value.length > 0 ? value : undefined;
100
+ }
101
+ /**
102
+ * Parses a `Content-Length` header value.
103
+ *
104
+ * Returns `undefined` for an absent, non-numeric, negative, or non-integer value rather than
105
+ * guessing: a malformed length is not evidence of a size, and the streaming cap does not need
106
+ * it. It is a fast-reject path only.
107
+ * @internal
108
+ */
109
+ export function parseContentLength(value) {
110
+ if (value === undefined) {
111
+ return undefined;
112
+ }
113
+ const trimmed = value.trim();
114
+ if (!/^\d+$/.test(trimmed)) {
115
+ return undefined;
116
+ }
117
+ const parsed = Number(trimmed);
118
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
119
+ }
120
+ //# sourceMappingURL=contentType.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contentType.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/contentType.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { fail, Result, succeed } from '@fgv/ts-utils';\n\n/**\n * A media type split into its type and subtype, lowercased, with parameters removed.\n * @internal\n */\nexport interface IMediaType {\n readonly type: string;\n readonly subtype: string;\n}\n\n// RFC 7230 token. Deliberately not a permissive `[^/;]+`: an allowlist entry that is not a\n// well-formed media type should be reported to its author, not quietly compiled into a\n// pattern that never matches anything.\nconst TOKEN: RegExp = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/**\n * Parses `type/subtype`, ignoring any parameters and surrounding whitespace, and lowercasing\n * both halves.\n *\n * The parse is not as trivial as it looks, which is the argument for owning it once:\n * `text/html; charset=utf-8` must match `text/html`, matching is case-insensitive on both\n * halves, and parameters must be *stripped* rather than string-matched around. Every\n * hand-rolled version gets a different subset of that right, and the failure is silent.\n * @internal\n */\nexport function parseMediaType(value: string): Result<IMediaType> {\n const essence = value.split(';')[0].trim().toLowerCase();\n const slash = essence.indexOf('/');\n if (slash < 0) {\n return fail(`\"${value}\": not a media type — expected \"type/subtype\".`);\n }\n const type = essence.slice(0, slash);\n const subtype = essence.slice(slash + 1);\n if (!TOKEN.test(type) && type !== '*') {\n return fail(`\"${value}\": \"${type}\" is not a valid media type.`);\n }\n if (!TOKEN.test(subtype) && subtype !== '*') {\n return fail(`\"${value}\": \"${subtype}\" is not a valid media subtype.`);\n }\n return succeed({ type, subtype });\n}\n\n/**\n * Parses an allowlist entry, which may use `*` as a wildcard for the subtype (`text/*`) or for\n * both halves (a wildcard type with a wildcard subtype, which accepts anything). A wildcard\n * type paired with a *concrete* subtype is rejected: it reads as if it means something and\n * does not.\n * @internal\n */\nexport function parseMediaTypePattern(value: string): Result<IMediaType> {\n return parseMediaType(value).onSuccess((parsed) => {\n if (parsed.type === '*' && parsed.subtype !== '*') {\n return fail(`\"${value}\": a wildcard type requires a wildcard subtype.`);\n }\n return succeed(parsed);\n });\n}\n\n/**\n * Reports whether a parsed media type matches a parsed allowlist pattern.\n * @internal\n */\nexport function mediaTypeMatches(actual: IMediaType, pattern: IMediaType): boolean {\n if (pattern.type === '*') {\n return true;\n }\n if (pattern.type !== actual.type) {\n return false;\n }\n return pattern.subtype === '*' || pattern.subtype === actual.subtype;\n}\n\n// A parameter value is either a token or a quoted-string; the charset names that matter here\n// (`utf-8`, `iso-8859-1`, …) are tokens either way once the quotes come off.\nconst CHARSET_PARAM: RegExp = /;\\s*charset\\s*=\\s*(?:\"([^\"]*)\"|([^;\\s]*))/i;\n\n/**\n * Extracts the `charset` parameter from a `Content-Type` header value, lowercased.\n *\n * Returns `undefined` when the header is absent or carries no `charset`, which the caller\n * reads as \"use the default encoding\" — not as \"any encoding will do\".\n * @internal\n */\nexport function parseCharset(contentType: string | undefined): string | undefined {\n if (contentType === undefined) {\n return undefined;\n }\n const match = CHARSET_PARAM.exec(contentType);\n if (match === null) {\n return undefined;\n }\n const value = (match[1] ?? match[2]).trim().toLowerCase();\n return value.length > 0 ? value : undefined;\n}\n\n/**\n * Parses a `Content-Length` header value.\n *\n * Returns `undefined` for an absent, non-numeric, negative, or non-integer value rather than\n * guessing: a malformed length is not evidence of a size, and the streaming cap does not need\n * it. It is a fast-reject path only.\n * @internal\n */\nexport function parseContentLength(value: string | undefined): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n if (!/^\\d+$/.test(trimmed)) {\n return undefined;\n }\n const parsed = Number(trimmed);\n return Number.isSafeInteger(parsed) ? parsed : undefined;\n}\n"]}
@@ -0,0 +1,254 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ /**
21
+ * Composes the caller's cancellation signal with an overall deadline and a headers deadline
22
+ * into one `AbortSignal`, and remembers which of the three stopped the call.
23
+ *
24
+ * @remarks
25
+ * The composition is done with a plain `AbortController` and listeners rather than
26
+ * `AbortSignal.any`, so there is no runtime-version floor to state and no feature-detection
27
+ * branch that only one of the two runtimes would ever execute.
28
+ *
29
+ * The overall deadline covers the connect, the headers, and the body read, and it is what
30
+ * stops a response that dribbles one byte every 25 seconds — such a response passes every
31
+ * per-read check and never trips a connect timeout.
32
+ * @internal
33
+ */
34
+ export class DeadlineWatch {
35
+ constructor(timeoutMs, headersTimeoutMs, callerSignal) {
36
+ this._controller = new AbortController();
37
+ this._startedAt = Date.now();
38
+ this._timeoutMs = timeoutMs;
39
+ this._headersTimeoutMs = headersTimeoutMs;
40
+ this._callerSignal = callerSignal;
41
+ this._terminal = false;
42
+ this._inBodyPhase = false;
43
+ this._waiters = new Set();
44
+ this._onCallerAbort = () => this._stop('caller-aborted');
45
+ this._overallTimer = setTimeout(() => this._stop(this._inBodyPhase ? 'body' : 'overall'), timeoutMs);
46
+ this._headersTimer = setTimeout(() => this._stop('headers'), headersTimeoutMs);
47
+ if (callerSignal !== undefined) {
48
+ if (callerSignal.aborted) {
49
+ this._stop('caller-aborted');
50
+ }
51
+ else {
52
+ callerSignal.addEventListener('abort', this._onCallerAbort);
53
+ }
54
+ }
55
+ }
56
+ /** The composed signal to hand to the transport. */
57
+ get signal() {
58
+ return this._controller.signal;
59
+ }
60
+ /** Why the call was stopped, or `undefined` while it is still running. */
61
+ get cause() {
62
+ return this._cause;
63
+ }
64
+ /**
65
+ * Records that a new attempt is starting: the headers deadline is (re)armed from now.
66
+ *
67
+ * @remarks
68
+ * The headers deadline is **per attempt**, and a redirect walk makes more than one. Without a
69
+ * re-arm, {@link DeadlineWatch.headersReceived} on the first hop would retire the headers
70
+ * deadline for the whole call, leaving every later hop bounded only by the overall deadline —
71
+ * so a chain whose second host simply never answers would hang for the overall budget instead
72
+ * of failing as `timeout.phase === 'headers'`.
73
+ *
74
+ * Called before the address guard rather than before the connect, because guard evaluation
75
+ * (a DNS resolution, in the shipped guard) is part of the time a caller waits for a usable
76
+ * response and is deliberately inside this budget.
77
+ *
78
+ * The overall deadline is untouched: it spans the whole call, redirects included.
79
+ */
80
+ attemptStarted() {
81
+ if (this._cause !== undefined) {
82
+ return;
83
+ }
84
+ if (this._headersTimer !== undefined) {
85
+ clearTimeout(this._headersTimer);
86
+ }
87
+ this._headersTimer = setTimeout(() => this._stop('headers'), this._headersTimeoutMs);
88
+ this._inBodyPhase = false;
89
+ }
90
+ /**
91
+ * Records that response headers have arrived: the headers deadline no longer applies to this
92
+ * attempt, and a subsequent overall-deadline expiry is a body-phase timeout rather than an
93
+ * overall one.
94
+ */
95
+ headersReceived() {
96
+ if (this._headersTimer !== undefined) {
97
+ clearTimeout(this._headersTimer);
98
+ this._headersTimer = undefined;
99
+ }
100
+ this._inBodyPhase = true;
101
+ }
102
+ /**
103
+ * Races a promise against the deadlines. A stopped race reports the cause; the underlying
104
+ * promise is abandoned, not cancelled — the caller is responsible for releasing whatever
105
+ * resource it represents (a response body reader, in practice).
106
+ *
107
+ * @remarks
108
+ * The waiter is removed once the race settles. A body read calls this once per chunk, so a
109
+ * waiter set that only grew would be a leak proportional to the number of chunks — in the
110
+ * exact code path whose job is to bound what a hostile response can cost the process.
111
+ */
112
+ async race(promise) {
113
+ if (this._cause !== undefined) {
114
+ return { stopped: true, cause: this._cause };
115
+ }
116
+ // Definitely assigned: a Promise executor runs synchronously.
117
+ let waiter;
118
+ const stop = new Promise((resolve) => {
119
+ waiter = (cause) => resolve({ stopped: true, cause });
120
+ });
121
+ this._waiters.add(waiter);
122
+ try {
123
+ return await Promise.race([
124
+ promise.then((value) => ({ stopped: false, value })),
125
+ stop
126
+ ]);
127
+ }
128
+ finally {
129
+ this._waiters.delete(waiter);
130
+ }
131
+ }
132
+ /**
133
+ * Records that an attempt has ended and another may follow: the headers deadline is disarmed,
134
+ * and a stop that ended only *that attempt* is cleared.
135
+ *
136
+ * @remarks
137
+ * **The headers deadline is attempt-scoped and the overall deadline is not.** A retry attempt
138
+ * that timed out waiting for headers must not leave the whole call stopped — without this,
139
+ * retry could never answer a `timeout.phase === 'headers'` failure, which is precisely the
140
+ * failure retry exists for. The composed signal is replaced rather than reset, because an
141
+ * aborted `AbortSignal` cannot be un-aborted.
142
+ *
143
+ * The overall deadline and the caller's signal are **terminal** by contrast: neither is about
144
+ * one attempt, and clearing either would let a retry loop outlive the budget the caller set or
145
+ * ignore the cancellation the caller requested. A terminal stop survives this call, so the
146
+ * backoff that follows is answered immediately and the call ends.
147
+ *
148
+ * Called between attempts rather than at the start of one, so the interval spent in a backoff
149
+ * is bounded by the overall deadline alone — arming a headers deadline over a sleep during
150
+ * which no request is outstanding would stop the call for a response nobody is waiting for.
151
+ */
152
+ attemptEnded() {
153
+ if (this._headersTimer !== undefined) {
154
+ clearTimeout(this._headersTimer);
155
+ this._headersTimer = undefined;
156
+ }
157
+ // No attempt is in flight between attempts, so the call is not in its body phase — whatever
158
+ // the previous attempt reached. Without this, an attempt that failed *after* headers arrived
159
+ // (a body-read network error, a non-2xx) would leave the flag set through the backoff, and
160
+ // an overall-deadline expiry during that sleep would be reported as `timeout.phase: 'body'`
161
+ // while no bytes were being transferred at all. `phase` is part of the taxonomy's contract,
162
+ // and a phase that names the wrong thing is exactly the kind of small lie this primitive is
163
+ // built not to tell.
164
+ this._inBodyPhase = false;
165
+ if (this._terminal || this._cause === undefined) {
166
+ return;
167
+ }
168
+ this._cause = undefined;
169
+ this._controller = new AbortController();
170
+ }
171
+ /**
172
+ * How much of the overall deadline is left, in milliseconds, never below zero.
173
+ *
174
+ * @remarks
175
+ * Read by the retry scheduler, which must not sleep past a deadline it is already inside:
176
+ * "the overall budget is the ceiling" is only enforceable if the remaining budget is
177
+ * observable.
178
+ */
179
+ get remainingMs() {
180
+ return Math.max(0, this._timeoutMs - (Date.now() - this._startedAt));
181
+ }
182
+ /**
183
+ * Waits for the given number of milliseconds, or until the call is stopped — whichever comes
184
+ * first.
185
+ *
186
+ * @remarks
187
+ * Backing the retry delay with the same watch the request races against is what keeps a
188
+ * caller's `abort()` responsive *between* attempts. A bare `setTimeout` would leave a caller
189
+ * who cancelled during a five-second backoff waiting out the full delay before being told the
190
+ * call was aborted.
191
+ *
192
+ * The timer is cleared however the wait ends, so a stopped delay leaves nothing pending —
193
+ * which matters in a test runner, where a stray timer keeps the process alive.
194
+ */
195
+ async delay(ms) {
196
+ let timer;
197
+ try {
198
+ return await this.race(new Promise((resolve) => {
199
+ timer = setTimeout(() => resolve(true), ms);
200
+ }));
201
+ }
202
+ finally {
203
+ if (timer !== undefined) {
204
+ clearTimeout(timer);
205
+ }
206
+ }
207
+ }
208
+ /** Builds the failure reason corresponding to why the call was stopped. */
209
+ toFailureReason(cause) {
210
+ if (cause === 'caller-aborted') {
211
+ return { kind: 'aborted' };
212
+ }
213
+ return {
214
+ kind: 'timeout',
215
+ phase: cause,
216
+ elapsedMs: Date.now() - this._startedAt,
217
+ limitMs: cause === 'headers' ? this._headersTimeoutMs : this._timeoutMs
218
+ };
219
+ }
220
+ /** Clears timers and listeners. Safe to call more than once. */
221
+ dispose() {
222
+ if (this._overallTimer !== undefined) {
223
+ clearTimeout(this._overallTimer);
224
+ this._overallTimer = undefined;
225
+ }
226
+ if (this._headersTimer !== undefined) {
227
+ clearTimeout(this._headersTimer);
228
+ this._headersTimer = undefined;
229
+ }
230
+ if (this._callerSignal !== undefined) {
231
+ this._callerSignal.removeEventListener('abort', this._onCallerAbort);
232
+ }
233
+ this._waiters.clear();
234
+ }
235
+ _stop(cause) {
236
+ // First cause wins. The overall and headers deadlines can be scheduled for the same instant,
237
+ // and a caller can abort while one of them is already firing; reporting the second would
238
+ // rewrite a phase the caller has arguably already been told about.
239
+ if (this._cause !== undefined) {
240
+ return;
241
+ }
242
+ this._cause = cause;
243
+ // Every cause but the per-attempt headers deadline ends the call: the overall deadline is
244
+ // the whole budget, and the caller's signal is the caller's decision.
245
+ this._terminal = cause !== 'headers';
246
+ const waiters = Array.from(this._waiters);
247
+ this._waiters.clear();
248
+ this._controller.abort();
249
+ for (const waiter of waiters) {
250
+ waiter(cause);
251
+ }
252
+ }
253
+ }
254
+ //# sourceMappingURL=deadline.js.map