@fgv/ts-extras 5.1.0-45 → 5.1.0-47

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 (141) hide show
  1. package/README.md +90 -0
  2. package/dist/index.browser.js +3 -1
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/packlets/ai-assist/index.js +1 -1
  7. package/dist/packlets/ai-assist/index.js.map +1 -1
  8. package/dist/packlets/ai-assist/jsonResponse.js +270 -7
  9. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -1
  10. package/dist/packlets/ai-assist/model.js +14 -0
  11. package/dist/packlets/ai-assist/model.js.map +1 -1
  12. package/dist/packlets/ai-assist/registry.js +57 -14
  13. package/dist/packlets/ai-assist/registry.js.map +1 -1
  14. package/dist/packlets/safer-fetch/addressClassification.js +438 -0
  15. package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
  16. package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
  17. package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
  18. package/dist/packlets/safer-fetch/contentType.js +120 -0
  19. package/dist/packlets/safer-fetch/contentType.js.map +1 -0
  20. package/dist/packlets/safer-fetch/deadline.js +254 -0
  21. package/dist/packlets/safer-fetch/deadline.js.map +1 -0
  22. package/dist/packlets/safer-fetch/defaults.js +131 -0
  23. package/dist/packlets/safer-fetch/defaults.js.map +1 -0
  24. package/dist/packlets/safer-fetch/failureReason.js +21 -0
  25. package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
  26. package/dist/packlets/safer-fetch/guards.js +148 -0
  27. package/dist/packlets/safer-fetch/guards.js.map +1 -0
  28. package/dist/packlets/safer-fetch/index.browser.js +58 -0
  29. package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
  30. package/dist/packlets/safer-fetch/index.js +52 -0
  31. package/dist/packlets/safer-fetch/index.js.map +1 -0
  32. package/dist/packlets/safer-fetch/model.js +21 -0
  33. package/dist/packlets/safer-fetch/model.js.map +1 -0
  34. package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
  35. package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  36. package/dist/packlets/safer-fetch/redirect.js +93 -0
  37. package/dist/packlets/safer-fetch/redirect.js.map +1 -0
  38. package/dist/packlets/safer-fetch/retry.js +188 -0
  39. package/dist/packlets/safer-fetch/retry.js.map +1 -0
  40. package/dist/packlets/safer-fetch/saferFetch.js +755 -0
  41. package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
  42. package/dist/packlets/safer-fetch/transport.js +50 -0
  43. package/dist/packlets/safer-fetch/transport.js.map +1 -0
  44. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  45. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  46. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +16 -1
  47. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  48. package/dist/ts-extras.d.ts +1590 -13
  49. package/lib/index.browser.d.ts +2 -1
  50. package/lib/index.browser.d.ts.map +1 -1
  51. package/lib/index.browser.js +4 -1
  52. package/lib/index.browser.js.map +1 -1
  53. package/lib/index.d.ts +2 -1
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js +3 -1
  56. package/lib/index.js.map +1 -1
  57. package/lib/packlets/ai-assist/index.d.ts +1 -1
  58. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/index.js +3 -2
  60. package/lib/packlets/ai-assist/index.js.map +1 -1
  61. package/lib/packlets/ai-assist/jsonResponse.d.ts +96 -0
  62. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/jsonResponse.js +271 -7
  64. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -1
  65. package/lib/packlets/ai-assist/model.d.ts +43 -0
  66. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/model.js +14 -0
  68. package/lib/packlets/ai-assist/model.js.map +1 -1
  69. package/lib/packlets/ai-assist/registry.d.ts +26 -6
  70. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/registry.js +57 -14
  72. package/lib/packlets/ai-assist/registry.js.map +1 -1
  73. package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
  74. package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
  75. package/lib/packlets/safer-fetch/addressClassification.js +441 -0
  76. package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
  77. package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
  78. package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
  79. package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
  80. package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
  81. package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
  82. package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
  83. package/lib/packlets/safer-fetch/contentType.js +127 -0
  84. package/lib/packlets/safer-fetch/contentType.js.map +1 -0
  85. package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
  86. package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
  87. package/lib/packlets/safer-fetch/deadline.js +258 -0
  88. package/lib/packlets/safer-fetch/deadline.js.map +1 -0
  89. package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
  90. package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
  91. package/lib/packlets/safer-fetch/defaults.js +134 -0
  92. package/lib/packlets/safer-fetch/defaults.js.map +1 -0
  93. package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
  94. package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
  95. package/lib/packlets/safer-fetch/failureReason.js +22 -0
  96. package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
  97. package/lib/packlets/safer-fetch/guards.d.ts +62 -0
  98. package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
  99. package/lib/packlets/safer-fetch/guards.js +153 -0
  100. package/lib/packlets/safer-fetch/guards.js.map +1 -0
  101. package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
  102. package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
  103. package/lib/packlets/safer-fetch/index.browser.js +82 -0
  104. package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
  105. package/lib/packlets/safer-fetch/index.d.ts +27 -0
  106. package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
  107. package/lib/packlets/safer-fetch/index.js +78 -0
  108. package/lib/packlets/safer-fetch/index.js.map +1 -0
  109. package/lib/packlets/safer-fetch/model.d.ts +373 -0
  110. package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
  111. package/lib/packlets/safer-fetch/model.js +22 -0
  112. package/lib/packlets/safer-fetch/model.js.map +1 -0
  113. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
  114. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
  115. package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
  116. package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  117. package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
  118. package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
  119. package/lib/packlets/safer-fetch/redirect.js +98 -0
  120. package/lib/packlets/safer-fetch/redirect.js.map +1 -0
  121. package/lib/packlets/safer-fetch/retry.d.ts +162 -0
  122. package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
  123. package/lib/packlets/safer-fetch/retry.js +197 -0
  124. package/lib/packlets/safer-fetch/retry.js.map +1 -0
  125. package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
  126. package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
  127. package/lib/packlets/safer-fetch/saferFetch.js +760 -0
  128. package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
  129. package/lib/packlets/safer-fetch/transport.d.ts +18 -0
  130. package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
  131. package/lib/packlets/safer-fetch/transport.js +53 -0
  132. package/lib/packlets/safer-fetch/transport.js.map +1 -0
  133. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +54 -6
  134. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -1
  135. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  136. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  137. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +26 -1
  138. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -1
  139. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +17 -1
  140. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  141. package/package.json +17 -7
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Which deadline a {@link SaferFetch.FetchFailureReason | timeout failure} belongs to.
3
+ *
4
+ * - `'headers'`: the per-attempt headers deadline elapsed before the server sent
5
+ * response headers — the host is not answering.
6
+ * - `'body'`: the overall deadline elapsed while the response body was being read
7
+ * — the host answered and then trickled (the slowloris shape).
8
+ * - `'overall'`: the overall deadline elapsed before response headers arrived.
9
+ * @public
10
+ */
11
+ export type FetchTimeoutPhase = 'headers' | 'body' | 'overall';
12
+ /**
13
+ * Machine-readable reason a safer-fetch call failed, carried as the detail of the
14
+ * `DetailedResult` every entry point returns.
15
+ *
16
+ * @remarks
17
+ * **This value is an internal-network scanning oracle. Do not echo it, or any string
18
+ * derived from it, to an untrusted caller.** A `'blocked-by-guard'` detail names the
19
+ * URL, the hop, and the guard that rejected it; an attacker who can influence the
20
+ * request URL and observe the error learns whether an internal host exists. Log the
21
+ * detail; return a coarse code (the `kind` alone, or a generic message) to callers.
22
+ *
23
+ * Following the `AiAssist.JsonParseFailureReason` precedent in this package: a `kind`
24
+ * discriminant with per-kind payload fields, and `'unknown'` as an honest catch-all
25
+ * rather than a guess.
26
+ * @public
27
+ */
28
+ export type FetchFailureReason =
29
+ /** The supplied URL could not be parsed, or its scheme is not `http:` or `https:`. */
30
+ {
31
+ readonly kind: 'invalid-url';
32
+ readonly url: string;
33
+ readonly detail: string;
34
+ }
35
+ /**
36
+ * A guard refused. `hop` is 0-based; hop 0 is the caller's URL, and `guard` names which
37
+ * guard rejected — which is what makes "did the address check run, and was it the thing that
38
+ * said no?" answerable from the failure alone. A content-type rejection is reported as
39
+ * `'unsupported-content-type'` instead, because that case carries a more useful payload.
40
+ */
41
+ | {
42
+ readonly kind: 'blocked-by-guard';
43
+ readonly url: string;
44
+ readonly hop: number;
45
+ readonly guard: string;
46
+ readonly detail: string;
47
+ }
48
+ /**
49
+ * A redirect status was received that this call would not follow: the policy is `'reject'`,
50
+ * or the response carried no usable `Location`, or following it would revisit a URL already
51
+ * in the chain.
52
+ *
53
+ * @remarks
54
+ * The three are deliberately one kind. Splitting them would buy a caller almost nothing and
55
+ * would widen the scanning oracle this taxonomy already is — and the two follow-time cases
56
+ * are facts about the chain the redirecting server produced, not about the network behind
57
+ * this process. "The chain got too long" is a different question and stays
58
+ * `'too-many-redirects'`.
59
+ *
60
+ * `url` is always the URL that **issued** the rejected redirect — the hop this call actually
61
+ * requested and got a 3xx back from — never the `Location` target it pointed at. That holds
62
+ * for all three cases, including the revisit case, where the target is the URL already in the
63
+ * chain and naming it here would make the same field mean two different things. The target is
64
+ * named in the message instead. `status` is the redirect status that was received.
65
+ */
66
+ | {
67
+ readonly kind: 'redirect-rejected';
68
+ readonly url: string;
69
+ readonly status: number;
70
+ }
71
+ /**
72
+ * The platform returned an opaque redirect, whose `Location` is not readable. This is
73
+ * what a browser yields for `redirect: 'manual'`; the hop cannot be inspected at all.
74
+ */
75
+ | {
76
+ readonly kind: 'redirect-opaque';
77
+ }
78
+ /** The redirect hop budget was exhausted. */
79
+ | {
80
+ readonly kind: 'too-many-redirects';
81
+ readonly hops: number;
82
+ readonly limit: number;
83
+ }
84
+ /** One of the deadlines elapsed. Distinct from `'aborted'`, which is the caller's signal. */
85
+ | {
86
+ readonly kind: 'timeout';
87
+ readonly phase: FetchTimeoutPhase;
88
+ readonly elapsedMs: number;
89
+ readonly limitMs: number;
90
+ }
91
+ /** The caller's `AbortSignal` fired. Distinct from `'timeout'`, which is our deadline. */
92
+ | {
93
+ readonly kind: 'aborted';
94
+ }
95
+ /** The transport could not complete the request. */
96
+ | {
97
+ readonly kind: 'network';
98
+ readonly detail: string;
99
+ }
100
+ /**
101
+ * A non-2xx response. `bodyPreview` is **never populated in this release** — error bodies
102
+ * routinely echo request content, including credentials, so surfacing one has to be an
103
+ * explicit, length-capped opt-in rather than a default. The field is declared so that adding
104
+ * that opt-in later is additive.
105
+ */
106
+ | {
107
+ readonly kind: 'http-status';
108
+ readonly status: number;
109
+ readonly statusText: string;
110
+ readonly bodyPreview?: string;
111
+ }
112
+ /**
113
+ * The response exceeded the byte cap. `declared` is what `Content-Length` claimed, present
114
+ * only when the header was sent — and it counts *encoded* bytes where `bytesRead` counts
115
+ * *decoded* bytes, so a `declared` far below `bytesRead` is evidence of a compression bomb
116
+ * or a lying server rather than an arithmetic error.
117
+ */
118
+ | {
119
+ readonly kind: 'too-large';
120
+ readonly bytesRead: number;
121
+ readonly limit: number;
122
+ readonly declared?: number;
123
+ }
124
+ /** A response-headers guard rejected the response's content type. */
125
+ | {
126
+ readonly kind: 'unsupported-content-type';
127
+ readonly contentType?: string;
128
+ readonly accepted: ReadonlyArray<string>;
129
+ }
130
+ /** The response bytes could not be decoded to text with the indicated charset. */
131
+ | {
132
+ readonly kind: 'decode';
133
+ readonly detail: string;
134
+ }
135
+ /** The decoded text could not be parsed, or failed the caller's converter. */
136
+ | {
137
+ readonly kind: 'parse';
138
+ readonly detail: string;
139
+ }
140
+ /**
141
+ * Anything else — including invalid options and a guard or transport that violated its
142
+ * contract. Reports what it knows rather than guessing at a more specific kind.
143
+ */
144
+ | {
145
+ readonly kind: 'unknown';
146
+ readonly detail: string;
147
+ };
148
+ //# sourceMappingURL=failureReason.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failureReason.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/failureReason.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,kBAAkB;AAC5B,sFAAsF;AACpF;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACjF;;;;;GAKG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AACH;;;;;;;;;;;;;;;;;GAiBG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACvF;;;GAGG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE;AACtC,6CAA6C;GAC3C;IAAE,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACxF,6FAA6F;GAC3F;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AACH,0FAA0F;GACxF;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AAC9B,oDAAoD;GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACvD;;;;;GAKG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AACH;;;;;GAKG;GACD;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AACH,qEAAqE;GACnE;IACE,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC1C;AACH,kFAAkF;GAChF;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACtD,8EAA8E;GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACrD;;;GAGG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ //# sourceMappingURL=failureReason.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failureReason.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/failureReason.ts"],"names":[],"mappings":";AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY","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/**\n * Which deadline a {@link SaferFetch.FetchFailureReason | timeout failure} belongs to.\n *\n * - `'headers'`: the per-attempt headers deadline elapsed before the server sent\n * response headers — the host is not answering.\n * - `'body'`: the overall deadline elapsed while the response body was being read\n * — the host answered and then trickled (the slowloris shape).\n * - `'overall'`: the overall deadline elapsed before response headers arrived.\n * @public\n */\nexport type FetchTimeoutPhase = 'headers' | 'body' | 'overall';\n\n/**\n * Machine-readable reason a safer-fetch call failed, carried as the detail of the\n * `DetailedResult` every entry point returns.\n *\n * @remarks\n * **This value is an internal-network scanning oracle. Do not echo it, or any string\n * derived from it, to an untrusted caller.** A `'blocked-by-guard'` detail names the\n * URL, the hop, and the guard that rejected it; an attacker who can influence the\n * request URL and observe the error learns whether an internal host exists. Log the\n * detail; return a coarse code (the `kind` alone, or a generic message) to callers.\n *\n * Following the `AiAssist.JsonParseFailureReason` precedent in this package: a `kind`\n * discriminant with per-kind payload fields, and `'unknown'` as an honest catch-all\n * rather than a guess.\n * @public\n */\nexport type FetchFailureReason =\n /** The supplied URL could not be parsed, or its scheme is not `http:` or `https:`. */\n | { readonly kind: 'invalid-url'; readonly url: string; readonly detail: string }\n /**\n * A guard refused. `hop` is 0-based; hop 0 is the caller's URL, and `guard` names which\n * guard rejected — which is what makes \"did the address check run, and was it the thing that\n * said no?\" answerable from the failure alone. A content-type rejection is reported as\n * `'unsupported-content-type'` instead, because that case carries a more useful payload.\n */\n | {\n readonly kind: 'blocked-by-guard';\n readonly url: string;\n readonly hop: number;\n readonly guard: string;\n readonly detail: string;\n }\n /**\n * A redirect status was received that this call would not follow: the policy is `'reject'`,\n * or the response carried no usable `Location`, or following it would revisit a URL already\n * in the chain.\n *\n * @remarks\n * The three are deliberately one kind. Splitting them would buy a caller almost nothing and\n * would widen the scanning oracle this taxonomy already is — and the two follow-time cases\n * are facts about the chain the redirecting server produced, not about the network behind\n * this process. \"The chain got too long\" is a different question and stays\n * `'too-many-redirects'`.\n *\n * `url` is always the URL that **issued** the rejected redirect — the hop this call actually\n * requested and got a 3xx back from — never the `Location` target it pointed at. That holds\n * for all three cases, including the revisit case, where the target is the URL already in the\n * chain and naming it here would make the same field mean two different things. The target is\n * named in the message instead. `status` is the redirect status that was received.\n */\n | { readonly kind: 'redirect-rejected'; readonly url: string; readonly status: number }\n /**\n * The platform returned an opaque redirect, whose `Location` is not readable. This is\n * what a browser yields for `redirect: 'manual'`; the hop cannot be inspected at all.\n */\n | { readonly kind: 'redirect-opaque' }\n /** The redirect hop budget was exhausted. */\n | { readonly kind: 'too-many-redirects'; readonly hops: number; readonly limit: number }\n /** One of the deadlines elapsed. Distinct from `'aborted'`, which is the caller's signal. */\n | {\n readonly kind: 'timeout';\n readonly phase: FetchTimeoutPhase;\n readonly elapsedMs: number;\n readonly limitMs: number;\n }\n /** The caller's `AbortSignal` fired. Distinct from `'timeout'`, which is our deadline. */\n | { readonly kind: 'aborted' }\n /** The transport could not complete the request. */\n | { readonly kind: 'network'; readonly detail: string }\n /**\n * A non-2xx response. `bodyPreview` is **never populated in this release** — error bodies\n * routinely echo request content, including credentials, so surfacing one has to be an\n * explicit, length-capped opt-in rather than a default. The field is declared so that adding\n * that opt-in later is additive.\n */\n | {\n readonly kind: 'http-status';\n readonly status: number;\n readonly statusText: string;\n readonly bodyPreview?: string;\n }\n /**\n * The response exceeded the byte cap. `declared` is what `Content-Length` claimed, present\n * only when the header was sent — and it counts *encoded* bytes where `bytesRead` counts\n * *decoded* bytes, so a `declared` far below `bytesRead` is evidence of a compression bomb\n * or a lying server rather than an arithmetic error.\n */\n | {\n readonly kind: 'too-large';\n readonly bytesRead: number;\n readonly limit: number;\n readonly declared?: number;\n }\n /** A response-headers guard rejected the response's content type. */\n | {\n readonly kind: 'unsupported-content-type';\n readonly contentType?: string;\n readonly accepted: ReadonlyArray<string>;\n }\n /** The response bytes could not be decoded to text with the indicated charset. */\n | { readonly kind: 'decode'; readonly detail: string }\n /** The decoded text could not be parsed, or failed the caller's converter. */\n | { readonly kind: 'parse'; readonly detail: string }\n /**\n * Anything else — including invalid options and a guard or transport that violated its\n * contract. Reports what it knows rather than guessing at a more specific kind.\n */\n | { readonly kind: 'unknown'; readonly detail: string };\n"]}
@@ -0,0 +1,62 @@
1
+ import { Result } from '@fgv/ts-utils';
2
+ import type { IAddressGuard, IResolvedGuards, IResponseHeadersGuard, ISaferFetchOptions } from './model';
3
+ /**
4
+ * An address guard that permits every address.
5
+ *
6
+ * @remarks
7
+ * **This is the absence of the SSRF guarantee, given a name.** It performs no address
8
+ * classification, no DNS resolution, and no scheme or port narrowing beyond the core's
9
+ * refusal of non-`http(s)` schemes. A call site using it is reachable at any address the
10
+ * process can route to, including cloud metadata endpoints, loopback admin ports, and
11
+ * RFC-1918 hosts.
12
+ *
13
+ * It ships anyway, for two reasons. It is the honest and only possible choice in a browser,
14
+ * where neither DNS resolution nor redirect interposition exists. And omitting it would not
15
+ * make anyone safer — it would make consumers hand-roll something worse. It is deliberately
16
+ * named to be uncomfortable in review and to grep as a distinct posture.
17
+ *
18
+ * This is the **guard-layer** factory: it returns an {@link SaferFetch.IAddressGuard}, which is
19
+ * what an entry point's `addressGuard` option takes. A pure address *policy* — the synchronous,
20
+ * address-list-in classifier a resolving guard delegates to — is a different layer and needs a
21
+ * distinct name.
22
+ * @public
23
+ */
24
+ export declare function allowAnyAddress(): IAddressGuard;
25
+ /**
26
+ * A response-headers guard that accepts only the listed media types.
27
+ *
28
+ * @remarks
29
+ * Rejecting on `Content-Type` is strictly cheaper than capping mid-read — a header comparison
30
+ * instead of a partial body transfer — so a consumer ingesting URLs wants it on every call. It
31
+ * is also what keeps an HTML error page served with a `200` from being parsed as JSON twenty
32
+ * frames away from where it went wrong.
33
+ *
34
+ * Entries are media types, optionally with a wildcard subtype (`text/*`); a wildcard type
35
+ * requires a wildcard subtype. Matching is case-insensitive and ignores parameters, so
36
+ * `text/html` accepts `text/html; charset=utf-8`. A response with **no** `Content-Type` is
37
+ * rejected: an untyped response has not satisfied a content-type allowlist.
38
+ *
39
+ * Construction is fallible because a malformed entry in a security-adjacent allowlist must be
40
+ * reported to its author rather than silently compiled into a pattern that never matches.
41
+ *
42
+ * @param types - Accepted media types. Must be non-empty.
43
+ * @public
44
+ */
45
+ export declare function allowContentTypes(types: ReadonlyArray<string>): Result<IResponseHeadersGuard>;
46
+ /**
47
+ * Resolves the guards a call will use, applying defaults once at the boundary so that no
48
+ * downstream code path branches on a guard's absence.
49
+ *
50
+ * @remarks
51
+ * Every optional guard resolves with `??`, uniformly. That uniformity is the point: a
52
+ * `!== undefined` test in one slot where its siblings use `??` lets a `null` — from a
53
+ * JavaScript caller, or through an `unknown` escape hatch — pass straight through and install
54
+ * itself as a guard, which is a real defect that has shipped in this repo before. A `null`
55
+ * guard here is treated as *absent*, never as *installed*.
56
+ *
57
+ * `addressGuard` has no default, so a `null` or missing value is a hard failure rather than a
58
+ * silent passthrough.
59
+ * @internal
60
+ */
61
+ export declare function resolveGuards(options: ISaferFetchOptions): Result<IResolvedGuards>;
62
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/guards.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAoB,MAAM,EAAW,MAAM,eAAe,CAAC;AAGlE,OAAO,KAAK,EACV,aAAa,EAIb,eAAe,EAEf,qBAAqB,EACrB,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,IAAI,aAAa,CAa/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,qBAAqB,CAAC,CA6B7F;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC,eAAe,CAAC,CAclF"}
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.allowAnyAddress = allowAnyAddress;
23
+ exports.allowContentTypes = allowContentTypes;
24
+ exports.resolveGuards = resolveGuards;
25
+ const ts_utils_1 = require("@fgv/ts-utils");
26
+ const contentType_1 = require("./contentType");
27
+ /**
28
+ * An address guard that permits every address.
29
+ *
30
+ * @remarks
31
+ * **This is the absence of the SSRF guarantee, given a name.** It performs no address
32
+ * classification, no DNS resolution, and no scheme or port narrowing beyond the core's
33
+ * refusal of non-`http(s)` schemes. A call site using it is reachable at any address the
34
+ * process can route to, including cloud metadata endpoints, loopback admin ports, and
35
+ * RFC-1918 hosts.
36
+ *
37
+ * It ships anyway, for two reasons. It is the honest and only possible choice in a browser,
38
+ * where neither DNS resolution nor redirect interposition exists. And omitting it would not
39
+ * make anyone safer — it would make consumers hand-roll something worse. It is deliberately
40
+ * named to be uncomfortable in review and to grep as a distinct posture.
41
+ *
42
+ * This is the **guard-layer** factory: it returns an {@link SaferFetch.IAddressGuard}, which is
43
+ * what an entry point's `addressGuard` option takes. A pure address *policy* — the synchronous,
44
+ * address-list-in classifier a resolving guard delegates to — is a different layer and needs a
45
+ * distinct name.
46
+ * @public
47
+ */
48
+ function allowAnyAddress() {
49
+ return {
50
+ name: 'allowAnyAddress',
51
+ check: async (chain) => {
52
+ var _a;
53
+ const hop = (_a = chain[chain.length - 1]) !== null && _a !== void 0 ? _a : undefined;
54
+ if (hop === undefined) {
55
+ return (0, ts_utils_1.fail)('allowAnyAddress: hop chain is empty.');
56
+ }
57
+ // pinnedAddress is deliberately left undefined: this guard validates nothing, so it has
58
+ // no address to pin, and a transport must never be told otherwise.
59
+ return (0, ts_utils_1.succeed)({ url: hop.url });
60
+ }
61
+ };
62
+ }
63
+ /**
64
+ * A response-headers guard that accepts only the listed media types.
65
+ *
66
+ * @remarks
67
+ * Rejecting on `Content-Type` is strictly cheaper than capping mid-read — a header comparison
68
+ * instead of a partial body transfer — so a consumer ingesting URLs wants it on every call. It
69
+ * is also what keeps an HTML error page served with a `200` from being parsed as JSON twenty
70
+ * frames away from where it went wrong.
71
+ *
72
+ * Entries are media types, optionally with a wildcard subtype (`text/*`); a wildcard type
73
+ * requires a wildcard subtype. Matching is case-insensitive and ignores parameters, so
74
+ * `text/html` accepts `text/html; charset=utf-8`. A response with **no** `Content-Type` is
75
+ * rejected: an untyped response has not satisfied a content-type allowlist.
76
+ *
77
+ * Construction is fallible because a malformed entry in a security-adjacent allowlist must be
78
+ * reported to its author rather than silently compiled into a pattern that never matches.
79
+ *
80
+ * @param types - Accepted media types. Must be non-empty.
81
+ * @public
82
+ */
83
+ function allowContentTypes(types) {
84
+ if (types.length === 0) {
85
+ return (0, ts_utils_1.fail)('allowContentTypes: at least one media type is required.');
86
+ }
87
+ return (0, ts_utils_1.mapResults)(types.map((t) => (0, contentType_1.parseMediaTypePattern)(t)))
88
+ .withErrorFormat((message) => `allowContentTypes: ${message}`)
89
+ .onSuccess((patterns) => {
90
+ const accepted = patterns.map((p) => `${p.type}/${p.subtype}`);
91
+ return (0, ts_utils_1.succeed)({
92
+ name: 'allowContentTypes',
93
+ acceptedContentTypes: accepted,
94
+ check: async (head) => {
95
+ var _a;
96
+ const contentType = (_a = head.contentType) !== null && _a !== void 0 ? _a : undefined;
97
+ if (contentType === undefined) {
98
+ return (0, ts_utils_1.fail)(`response has no content-type; expected one of [${accepted.join(', ')}].`);
99
+ }
100
+ return (0, contentType_1.parseMediaType)(contentType)
101
+ .withErrorFormat((message) => `content-type ${message}`)
102
+ .onSuccess((actual) => {
103
+ if (patterns.some((p) => (0, contentType_1.mediaTypeMatches)(actual, p))) {
104
+ return (0, ts_utils_1.succeed)(true);
105
+ }
106
+ return (0, ts_utils_1.fail)(`content-type "${contentType}" is not accepted; expected one of [${accepted.join(', ')}].`);
107
+ });
108
+ }
109
+ });
110
+ });
111
+ }
112
+ const passthroughRequestGuard = {
113
+ name: 'passthrough',
114
+ check: async (request) => (0, ts_utils_1.succeed)(request)
115
+ };
116
+ const passthroughResponseHeadersGuard = {
117
+ name: 'passthrough',
118
+ check: async () => (0, ts_utils_1.succeed)(true)
119
+ };
120
+ const passthroughResponseBodyGuard = {
121
+ name: 'passthrough',
122
+ check: async () => (0, ts_utils_1.succeed)(true)
123
+ };
124
+ /**
125
+ * Resolves the guards a call will use, applying defaults once at the boundary so that no
126
+ * downstream code path branches on a guard's absence.
127
+ *
128
+ * @remarks
129
+ * Every optional guard resolves with `??`, uniformly. That uniformity is the point: a
130
+ * `!== undefined` test in one slot where its siblings use `??` lets a `null` — from a
131
+ * JavaScript caller, or through an `unknown` escape hatch — pass straight through and install
132
+ * itself as a guard, which is a real defect that has shipped in this repo before. A `null`
133
+ * guard here is treated as *absent*, never as *installed*.
134
+ *
135
+ * `addressGuard` has no default, so a `null` or missing value is a hard failure rather than a
136
+ * silent passthrough.
137
+ * @internal
138
+ */
139
+ function resolveGuards(options) {
140
+ var _a, _b, _c, _d;
141
+ const address = (_a = options.addressGuard) !== null && _a !== void 0 ? _a : undefined;
142
+ if (address === undefined) {
143
+ return (0, ts_utils_1.fail)('addressGuard is required and has no default. Supply a named factory — ' +
144
+ 'allowAnyAddress() is the explicit opt-out.');
145
+ }
146
+ return (0, ts_utils_1.succeed)({
147
+ address,
148
+ request: (_b = options.requestGuard) !== null && _b !== void 0 ? _b : passthroughRequestGuard,
149
+ responseHeaders: (_c = options.responseHeadersGuard) !== null && _c !== void 0 ? _c : passthroughResponseHeadersGuard,
150
+ responseBody: (_d = options.responseBodyGuard) !== null && _d !== void 0 ? _d : passthroughResponseBodyGuard
151
+ });
152
+ }
153
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/guards.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, mapResults, Result, succeed } from '@fgv/ts-utils';\n\nimport { IMediaType, mediaTypeMatches, parseMediaType, parseMediaTypePattern } from './contentType';\nimport type {\n IAddressGuard,\n IGuardVerdict,\n IRequestGuard,\n IRequestHop,\n IResolvedGuards,\n IResponseBodyGuard,\n IResponseHeadersGuard,\n ISaferFetchOptions,\n ISaferFetchRequest,\n ISaferFetchResponseHead\n} from './model';\n\n/**\n * An address guard that permits every address.\n *\n * @remarks\n * **This is the absence of the SSRF guarantee, given a name.** It performs no address\n * classification, no DNS resolution, and no scheme or port narrowing beyond the core's\n * refusal of non-`http(s)` schemes. A call site using it is reachable at any address the\n * process can route to, including cloud metadata endpoints, loopback admin ports, and\n * RFC-1918 hosts.\n *\n * It ships anyway, for two reasons. It is the honest and only possible choice in a browser,\n * where neither DNS resolution nor redirect interposition exists. And omitting it would not\n * make anyone safer — it would make consumers hand-roll something worse. It is deliberately\n * named to be uncomfortable in review and to grep as a distinct posture.\n *\n * This is the **guard-layer** factory: it returns an {@link SaferFetch.IAddressGuard}, which is\n * what an entry point's `addressGuard` option takes. A pure address *policy* — the synchronous,\n * address-list-in classifier a resolving guard delegates to — is a different layer and needs a\n * distinct name.\n * @public\n */\nexport function allowAnyAddress(): IAddressGuard {\n return {\n name: 'allowAnyAddress',\n check: async (chain: ReadonlyArray<IRequestHop>): Promise<Result<IGuardVerdict>> => {\n const hop = chain[chain.length - 1] ?? undefined;\n if (hop === undefined) {\n return fail('allowAnyAddress: hop chain is empty.');\n }\n // pinnedAddress is deliberately left undefined: this guard validates nothing, so it has\n // no address to pin, and a transport must never be told otherwise.\n return succeed({ url: hop.url });\n }\n };\n}\n\n/**\n * A response-headers guard that accepts only the listed media types.\n *\n * @remarks\n * Rejecting on `Content-Type` is strictly cheaper than capping mid-read — a header comparison\n * instead of a partial body transfer — so a consumer ingesting URLs wants it on every call. It\n * is also what keeps an HTML error page served with a `200` from being parsed as JSON twenty\n * frames away from where it went wrong.\n *\n * Entries are media types, optionally with a wildcard subtype (`text/*`); a wildcard type\n * requires a wildcard subtype. Matching is case-insensitive and ignores parameters, so\n * `text/html` accepts `text/html; charset=utf-8`. A response with **no** `Content-Type` is\n * rejected: an untyped response has not satisfied a content-type allowlist.\n *\n * Construction is fallible because a malformed entry in a security-adjacent allowlist must be\n * reported to its author rather than silently compiled into a pattern that never matches.\n *\n * @param types - Accepted media types. Must be non-empty.\n * @public\n */\nexport function allowContentTypes(types: ReadonlyArray<string>): Result<IResponseHeadersGuard> {\n if (types.length === 0) {\n return fail('allowContentTypes: at least one media type is required.');\n }\n return mapResults(types.map((t) => parseMediaTypePattern(t)))\n .withErrorFormat((message) => `allowContentTypes: ${message}`)\n .onSuccess((patterns: IMediaType[]) => {\n const accepted: ReadonlyArray<string> = patterns.map((p) => `${p.type}/${p.subtype}`);\n return succeed({\n name: 'allowContentTypes',\n acceptedContentTypes: accepted,\n check: async (head: ISaferFetchResponseHead): Promise<Result<true>> => {\n const contentType = head.contentType ?? undefined;\n if (contentType === undefined) {\n return fail(`response has no content-type; expected one of [${accepted.join(', ')}].`);\n }\n return parseMediaType(contentType)\n .withErrorFormat((message) => `content-type ${message}`)\n .onSuccess((actual) => {\n if (patterns.some((p) => mediaTypeMatches(actual, p))) {\n return succeed(true as const);\n }\n return fail(\n `content-type \"${contentType}\" is not accepted; expected one of [${accepted.join(', ')}].`\n );\n });\n }\n });\n });\n}\n\nconst passthroughRequestGuard: IRequestGuard = {\n name: 'passthrough',\n check: async (request: ISaferFetchRequest): Promise<Result<ISaferFetchRequest>> => succeed(request)\n};\n\nconst passthroughResponseHeadersGuard: IResponseHeadersGuard = {\n name: 'passthrough',\n check: async (): Promise<Result<true>> => succeed(true as const)\n};\n\nconst passthroughResponseBodyGuard: IResponseBodyGuard = {\n name: 'passthrough',\n check: async (): Promise<Result<true>> => succeed(true as const)\n};\n\n/**\n * Resolves the guards a call will use, applying defaults once at the boundary so that no\n * downstream code path branches on a guard's absence.\n *\n * @remarks\n * Every optional guard resolves with `??`, uniformly. That uniformity is the point: a\n * `!== undefined` test in one slot where its siblings use `??` lets a `null` — from a\n * JavaScript caller, or through an `unknown` escape hatch — pass straight through and install\n * itself as a guard, which is a real defect that has shipped in this repo before. A `null`\n * guard here is treated as *absent*, never as *installed*.\n *\n * `addressGuard` has no default, so a `null` or missing value is a hard failure rather than a\n * silent passthrough.\n * @internal\n */\nexport function resolveGuards(options: ISaferFetchOptions): Result<IResolvedGuards> {\n const address = options.addressGuard ?? undefined;\n if (address === undefined) {\n return fail(\n 'addressGuard is required and has no default. Supply a named factory — ' +\n 'allowAnyAddress() is the explicit opt-out.'\n );\n }\n return succeed({\n address,\n request: options.requestGuard ?? passthroughRequestGuard,\n responseHeaders: options.responseHeadersGuard ?? passthroughResponseHeadersGuard,\n responseBody: options.responseBodyGuard ?? passthroughResponseBodyGuard\n });\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * An HTTP fetch primitive with an explicit threat model (browser version).
3
+ *
4
+ * This barrel deliberately omits `blockPrivateNetworks` and the resolver seam beneath it: that
5
+ * module imports `node:dns/promises`, and exporting it here would pull a Node builtin into a
6
+ * browser bundle.
7
+ *
8
+ * The address-classification layer — `classifyAddress`, and the pure synchronous policies
9
+ * `allowAnyAddressPolicy` / `blockPrivateNetworksPolicy` — **is** exported here: it is pure
10
+ * arithmetic over parsed octets, it works identically in a browser, and it is useful for a
11
+ * URL-zero check. It is not a substitute for the guard, and cannot be: it classifies an address
12
+ * a caller already holds, while what the browser lacks is any way to learn the address a
13
+ * hostname resolves to.
14
+ *
15
+ * Note: the resolved-address (private-IP) guard and per-hop redirect revalidation are NOT
16
+ * available in a browser, and not for want of implementation. There is no browser API that
17
+ * returns a hostname's A/AAAA records, nothing in `fetch` or `Response` exposes the peer
18
+ * address, and `redirect: 'manual'` yields an opaque response whose `Location` is not
19
+ * readable — a `'validate-each-hop'` call there fails as `'redirect-opaque'` rather than
20
+ * quietly following anything. `allowAnyAddress()` is the honest choice, and its name says so.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ export { ALWAYS_STRIPPED_HEADERS, DEFAULT_HEADERS_TIMEOUT_MS, DEFAULT_MAX_REDIRECTS, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_BASE_DELAY_MS, DEFAULT_RETRY_MAX_DELAY_MS, DEFAULT_TIMEOUT_MS, IDEMPOTENT_METHODS, REDIRECT_STATUSES, RETRY_AFTER_STATUSES, RETRYABLE_HTTP_STATUSES, SUPPORTED_SCHEMES } from './defaults';
25
+ export type { FetchFailureReason, FetchTimeoutPhase } from './failureReason';
26
+ export type { IRetryPolicy } from './retry';
27
+ export type { IAddressGuard, IFetchTransport, IFetchTransportHints, IGuardVerdict, IRequestGuard, IRequestHop, IResolvedGuards, IResponseBodyGuard, IResponseHeadersGuard, ISaferFetchOptions, ISaferFetchRequest, ISaferFetchResponse, ISaferFetchResponseHead, SaferFetchMethod, SaferFetchRedirectPolicy } from './model';
28
+ export { allowAnyAddress, allowContentTypes } from './guards';
29
+ export { platformFetchTransport } from './transport';
30
+ export { saferFetchBytes, saferFetchJson, saferFetchText, type ISaferFetchJsonOptions } from './saferFetch';
31
+ export { allowAnyAddressPolicy, blockPrivateNetworksPolicy, type IAddressCheckVerdict, type IAddressPolicy, type IBlockPrivateNetworksOptions } from './addressPolicy';
32
+ export { classifyAddress, type AddressClassification, type AddressFamily, type IClassifiedAddress, type IEmbeddedIpv4, type Ipv4EmbeddingKind } from './addressClassification';
33
+ //# sourceMappingURL=index.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/index.browser.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC1B,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,YAAY,EACV,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAW5G,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.classifyAddress = exports.blockPrivateNetworksPolicy = exports.allowAnyAddressPolicy = exports.saferFetchText = exports.saferFetchJson = exports.saferFetchBytes = exports.platformFetchTransport = exports.allowContentTypes = exports.allowAnyAddress = exports.SUPPORTED_SCHEMES = exports.RETRYABLE_HTTP_STATUSES = exports.RETRY_AFTER_STATUSES = exports.REDIRECT_STATUSES = exports.IDEMPOTENT_METHODS = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_RETRY_MAX_DELAY_MS = exports.DEFAULT_RETRY_BASE_DELAY_MS = exports.DEFAULT_MAX_RESPONSE_BYTES = exports.DEFAULT_MAX_REDIRECTS = exports.DEFAULT_HEADERS_TIMEOUT_MS = exports.ALWAYS_STRIPPED_HEADERS = void 0;
23
+ /**
24
+ * An HTTP fetch primitive with an explicit threat model (browser version).
25
+ *
26
+ * This barrel deliberately omits `blockPrivateNetworks` and the resolver seam beneath it: that
27
+ * module imports `node:dns/promises`, and exporting it here would pull a Node builtin into a
28
+ * browser bundle.
29
+ *
30
+ * The address-classification layer — `classifyAddress`, and the pure synchronous policies
31
+ * `allowAnyAddressPolicy` / `blockPrivateNetworksPolicy` — **is** exported here: it is pure
32
+ * arithmetic over parsed octets, it works identically in a browser, and it is useful for a
33
+ * URL-zero check. It is not a substitute for the guard, and cannot be: it classifies an address
34
+ * a caller already holds, while what the browser lacks is any way to learn the address a
35
+ * hostname resolves to.
36
+ *
37
+ * Note: the resolved-address (private-IP) guard and per-hop redirect revalidation are NOT
38
+ * available in a browser, and not for want of implementation. There is no browser API that
39
+ * returns a hostname's A/AAAA records, nothing in `fetch` or `Response` exposes the peer
40
+ * address, and `redirect: 'manual'` yields an opaque response whose `Location` is not
41
+ * readable — a `'validate-each-hop'` call there fails as `'redirect-opaque'` rather than
42
+ * quietly following anything. `allowAnyAddress()` is the honest choice, and its name says so.
43
+ *
44
+ * @packageDocumentation
45
+ */
46
+ var defaults_1 = require("./defaults");
47
+ Object.defineProperty(exports, "ALWAYS_STRIPPED_HEADERS", { enumerable: true, get: function () { return defaults_1.ALWAYS_STRIPPED_HEADERS; } });
48
+ Object.defineProperty(exports, "DEFAULT_HEADERS_TIMEOUT_MS", { enumerable: true, get: function () { return defaults_1.DEFAULT_HEADERS_TIMEOUT_MS; } });
49
+ Object.defineProperty(exports, "DEFAULT_MAX_REDIRECTS", { enumerable: true, get: function () { return defaults_1.DEFAULT_MAX_REDIRECTS; } });
50
+ Object.defineProperty(exports, "DEFAULT_MAX_RESPONSE_BYTES", { enumerable: true, get: function () { return defaults_1.DEFAULT_MAX_RESPONSE_BYTES; } });
51
+ Object.defineProperty(exports, "DEFAULT_RETRY_BASE_DELAY_MS", { enumerable: true, get: function () { return defaults_1.DEFAULT_RETRY_BASE_DELAY_MS; } });
52
+ Object.defineProperty(exports, "DEFAULT_RETRY_MAX_DELAY_MS", { enumerable: true, get: function () { return defaults_1.DEFAULT_RETRY_MAX_DELAY_MS; } });
53
+ Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", { enumerable: true, get: function () { return defaults_1.DEFAULT_TIMEOUT_MS; } });
54
+ Object.defineProperty(exports, "IDEMPOTENT_METHODS", { enumerable: true, get: function () { return defaults_1.IDEMPOTENT_METHODS; } });
55
+ Object.defineProperty(exports, "REDIRECT_STATUSES", { enumerable: true, get: function () { return defaults_1.REDIRECT_STATUSES; } });
56
+ Object.defineProperty(exports, "RETRY_AFTER_STATUSES", { enumerable: true, get: function () { return defaults_1.RETRY_AFTER_STATUSES; } });
57
+ Object.defineProperty(exports, "RETRYABLE_HTTP_STATUSES", { enumerable: true, get: function () { return defaults_1.RETRYABLE_HTTP_STATUSES; } });
58
+ Object.defineProperty(exports, "SUPPORTED_SCHEMES", { enumerable: true, get: function () { return defaults_1.SUPPORTED_SCHEMES; } });
59
+ var guards_1 = require("./guards");
60
+ Object.defineProperty(exports, "allowAnyAddress", { enumerable: true, get: function () { return guards_1.allowAnyAddress; } });
61
+ Object.defineProperty(exports, "allowContentTypes", { enumerable: true, get: function () { return guards_1.allowContentTypes; } });
62
+ var transport_1 = require("./transport");
63
+ Object.defineProperty(exports, "platformFetchTransport", { enumerable: true, get: function () { return transport_1.platformFetchTransport; } });
64
+ var saferFetch_1 = require("./saferFetch");
65
+ Object.defineProperty(exports, "saferFetchBytes", { enumerable: true, get: function () { return saferFetch_1.saferFetchBytes; } });
66
+ Object.defineProperty(exports, "saferFetchJson", { enumerable: true, get: function () { return saferFetch_1.saferFetchJson; } });
67
+ Object.defineProperty(exports, "saferFetchText", { enumerable: true, get: function () { return saferFetch_1.saferFetchText; } });
68
+ // Runtime-agnostic and exported from both barrels: the address *classification* layer is pure
69
+ // arithmetic over parsed octets with no I/O, so it works in a browser exactly as it does on
70
+ // Node. It is useful there for a URL-zero check — a caller can refuse an IP-literal URL that
71
+ // classifies as private before ever calling `fetch`.
72
+ //
73
+ // **It cannot substitute for the resolved-address guard.** It classifies an address you already
74
+ // have; the guarantee the browser lacks is *obtaining* the address a hostname resolves to, which
75
+ // no browser API provides. `https://internal.example.com/` resolving to `10.0.0.5` is invisible
76
+ // to every function exported here.
77
+ var addressPolicy_1 = require("./addressPolicy");
78
+ Object.defineProperty(exports, "allowAnyAddressPolicy", { enumerable: true, get: function () { return addressPolicy_1.allowAnyAddressPolicy; } });
79
+ Object.defineProperty(exports, "blockPrivateNetworksPolicy", { enumerable: true, get: function () { return addressPolicy_1.blockPrivateNetworksPolicy; } });
80
+ var addressClassification_1 = require("./addressClassification");
81
+ Object.defineProperty(exports, "classifyAddress", { enumerable: true, get: function () { return addressClassification_1.classifyAddress; } });
82
+ //# sourceMappingURL=index.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/index.browser.ts"],"names":[],"mappings":";AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;;;AAEZ;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uCAaoB;AAZlB,mHAAA,uBAAuB,OAAA;AACvB,sHAAA,0BAA0B,OAAA;AAC1B,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,uHAAA,2BAA2B,OAAA;AAC3B,sHAAA,0BAA0B,OAAA;AAC1B,8GAAA,kBAAkB,OAAA;AAClB,8GAAA,kBAAkB,OAAA;AAClB,6GAAA,iBAAiB,OAAA;AACjB,gHAAA,oBAAoB,OAAA;AACpB,mHAAA,uBAAuB,OAAA;AACvB,6GAAA,iBAAiB,OAAA;AAwBnB,mCAA8D;AAArD,yGAAA,eAAe,OAAA;AAAE,2GAAA,iBAAiB,OAAA;AAE3C,yCAAqD;AAA5C,mHAAA,sBAAsB,OAAA;AAE/B,2CAA4G;AAAnG,6GAAA,eAAe,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,4GAAA,cAAc,OAAA;AAExD,8FAA8F;AAC9F,4FAA4F;AAC5F,6FAA6F;AAC7F,qDAAqD;AACrD,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,gGAAgG;AAChG,mCAAmC;AACnC,iDAMyB;AALvB,sHAAA,qBAAqB,OAAA;AACrB,2HAAA,0BAA0B,OAAA;AAM5B,iEAOiC;AAN/B,wHAAA,eAAe,OAAA","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/**\n * An HTTP fetch primitive with an explicit threat model (browser version).\n *\n * This barrel deliberately omits `blockPrivateNetworks` and the resolver seam beneath it: that\n * module imports `node:dns/promises`, and exporting it here would pull a Node builtin into a\n * browser bundle.\n *\n * The address-classification layer — `classifyAddress`, and the pure synchronous policies\n * `allowAnyAddressPolicy` / `blockPrivateNetworksPolicy` — **is** exported here: it is pure\n * arithmetic over parsed octets, it works identically in a browser, and it is useful for a\n * URL-zero check. It is not a substitute for the guard, and cannot be: it classifies an address\n * a caller already holds, while what the browser lacks is any way to learn the address a\n * hostname resolves to.\n *\n * Note: the resolved-address (private-IP) guard and per-hop redirect revalidation are NOT\n * available in a browser, and not for want of implementation. There is no browser API that\n * returns a hostname's A/AAAA records, nothing in `fetch` or `Response` exposes the peer\n * address, and `redirect: 'manual'` yields an opaque response whose `Location` is not\n * readable — a `'validate-each-hop'` call there fails as `'redirect-opaque'` rather than\n * quietly following anything. `allowAnyAddress()` is the honest choice, and its name says so.\n *\n * @packageDocumentation\n */\n\nexport {\n ALWAYS_STRIPPED_HEADERS,\n DEFAULT_HEADERS_TIMEOUT_MS,\n DEFAULT_MAX_REDIRECTS,\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_RETRY_BASE_DELAY_MS,\n DEFAULT_RETRY_MAX_DELAY_MS,\n DEFAULT_TIMEOUT_MS,\n IDEMPOTENT_METHODS,\n REDIRECT_STATUSES,\n RETRY_AFTER_STATUSES,\n RETRYABLE_HTTP_STATUSES,\n SUPPORTED_SCHEMES\n} from './defaults';\n\nexport type { FetchFailureReason, FetchTimeoutPhase } from './failureReason';\nexport type { IRetryPolicy } from './retry';\n\nexport type {\n IAddressGuard,\n IFetchTransport,\n IFetchTransportHints,\n IGuardVerdict,\n IRequestGuard,\n IRequestHop,\n IResolvedGuards,\n IResponseBodyGuard,\n IResponseHeadersGuard,\n ISaferFetchOptions,\n ISaferFetchRequest,\n ISaferFetchResponse,\n ISaferFetchResponseHead,\n SaferFetchMethod,\n SaferFetchRedirectPolicy\n} from './model';\n\nexport { allowAnyAddress, allowContentTypes } from './guards';\n\nexport { platformFetchTransport } from './transport';\n\nexport { saferFetchBytes, saferFetchJson, saferFetchText, type ISaferFetchJsonOptions } from './saferFetch';\n\n// Runtime-agnostic and exported from both barrels: the address *classification* layer is pure\n// arithmetic over parsed octets with no I/O, so it works in a browser exactly as it does on\n// Node. It is useful there for a URL-zero check — a caller can refuse an IP-literal URL that\n// classifies as private before ever calling `fetch`.\n//\n// **It cannot substitute for the resolved-address guard.** It classifies an address you already\n// have; the guarantee the browser lacks is *obtaining* the address a hostname resolves to, which\n// no browser API provides. `https://internal.example.com/` resolving to `10.0.0.5` is invisible\n// to every function exported here.\nexport {\n allowAnyAddressPolicy,\n blockPrivateNetworksPolicy,\n type IAddressCheckVerdict,\n type IAddressPolicy,\n type IBlockPrivateNetworksOptions\n} from './addressPolicy';\n\nexport {\n classifyAddress,\n type AddressClassification,\n type AddressFamily,\n type IClassifiedAddress,\n type IEmbeddedIpv4,\n type Ipv4EmbeddingKind\n} from './addressClassification';\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A safer `fetch` primitive with an explicit threat model.
3
+ *
4
+ * This is deliberately **not** a thin, unopinionated boundary over an upstream library. There
5
+ * is no upstream to wrap — `fetch` is a platform global — and the opinion *is* the product:
6
+ * the deadlines, the scheme refusal, the streaming size cap, the redirect posture, and the
7
+ * required address guard are the deliverable. A caller who strips the opinion out has `fetch`,
8
+ * which is where they started.
9
+ *
10
+ * The guiding constraint: *a primitive that advertises a guarantee it does not have is worse
11
+ * than five lines at a call site, because it transfers responsibility without transferring
12
+ * protection.* Every entry point's documentation names what it does **not** protect against
13
+ * next to what it does.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ export { ALWAYS_STRIPPED_HEADERS, DEFAULT_HEADERS_TIMEOUT_MS, DEFAULT_MAX_REDIRECTS, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_BASE_DELAY_MS, DEFAULT_RETRY_MAX_DELAY_MS, DEFAULT_TIMEOUT_MS, IDEMPOTENT_METHODS, REDIRECT_STATUSES, RETRY_AFTER_STATUSES, RETRYABLE_HTTP_STATUSES, SUPPORTED_SCHEMES } from './defaults';
18
+ export type { FetchFailureReason, FetchTimeoutPhase } from './failureReason';
19
+ export type { IRetryPolicy } from './retry';
20
+ export type { IAddressGuard, IFetchTransport, IFetchTransportHints, IGuardVerdict, IRequestGuard, IRequestHop, IResolvedGuards, IResponseBodyGuard, IResponseHeadersGuard, ISaferFetchOptions, ISaferFetchRequest, ISaferFetchResponse, ISaferFetchResponseHead, SaferFetchMethod, SaferFetchRedirectPolicy } from './model';
21
+ export { allowAnyAddress, allowContentTypes } from './guards';
22
+ export { platformFetchTransport } from './transport';
23
+ export { saferFetchBytes, saferFetchJson, saferFetchText, type ISaferFetchJsonOptions } from './saferFetch';
24
+ export { allowAnyAddressPolicy, blockPrivateNetworksPolicy, type IAddressCheckVerdict, type IAddressPolicy, type IBlockPrivateNetworksOptions } from './addressPolicy';
25
+ export { blockPrivateNetworks, nodeHostResolver, type HostResolver, type IBlockPrivateNetworksGuardOptions } from './nodeAddressGuard';
26
+ export { classifyAddress, type AddressClassification, type AddressFamily, type IClassifiedAddress, type IEmbeddedIpv4, type Ipv4EmbeddingKind } from './addressClassification';
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/index.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC1B,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,YAAY,EACV,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAQ5G,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,iCAAiC,EACvC,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC"}