@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.
- package/LICENSE +1 -1
- package/README.md +90 -0
- package/dist/index.browser.js +3 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/packlets/safer-fetch/addressClassification.js +438 -0
- package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
- package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/dist/packlets/safer-fetch/contentType.js +120 -0
- package/dist/packlets/safer-fetch/contentType.js.map +1 -0
- package/dist/packlets/safer-fetch/deadline.js +254 -0
- package/dist/packlets/safer-fetch/deadline.js.map +1 -0
- package/dist/packlets/safer-fetch/defaults.js +131 -0
- package/dist/packlets/safer-fetch/defaults.js.map +1 -0
- package/dist/packlets/safer-fetch/failureReason.js +21 -0
- package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
- package/dist/packlets/safer-fetch/guards.js +148 -0
- package/dist/packlets/safer-fetch/guards.js.map +1 -0
- package/dist/packlets/safer-fetch/index.browser.js +58 -0
- package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
- package/dist/packlets/safer-fetch/index.js +52 -0
- package/dist/packlets/safer-fetch/index.js.map +1 -0
- package/dist/packlets/safer-fetch/model.js +21 -0
- package/dist/packlets/safer-fetch/model.js.map +1 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
- package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/dist/packlets/safer-fetch/redirect.js +93 -0
- package/dist/packlets/safer-fetch/redirect.js.map +1 -0
- package/dist/packlets/safer-fetch/retry.js +188 -0
- package/dist/packlets/safer-fetch/retry.js.map +1 -0
- package/dist/packlets/safer-fetch/saferFetch.js +755 -0
- package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/dist/packlets/safer-fetch/transport.js +50 -0
- package/dist/packlets/safer-fetch/transport.js.map +1 -0
- package/dist/ts-extras.d.ts +1337 -0
- package/lib/index.browser.d.ts +2 -1
- package/lib/index.browser.d.ts.map +1 -1
- package/lib/index.browser.js +4 -1
- package/lib/index.browser.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
- package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressClassification.js +441 -0
- package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
- package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
- package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
- package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
- package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/contentType.js +127 -0
- package/lib/packlets/safer-fetch/contentType.js.map +1 -0
- package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
- package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/deadline.js +258 -0
- package/lib/packlets/safer-fetch/deadline.js.map +1 -0
- package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
- package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/defaults.js +134 -0
- package/lib/packlets/safer-fetch/defaults.js.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
- package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/failureReason.js +22 -0
- package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
- package/lib/packlets/safer-fetch/guards.d.ts +62 -0
- package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/guards.js +153 -0
- package/lib/packlets/safer-fetch/guards.js.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
- package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.browser.js +82 -0
- package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
- package/lib/packlets/safer-fetch/index.d.ts +27 -0
- package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/index.js +78 -0
- package/lib/packlets/safer-fetch/index.js.map +1 -0
- package/lib/packlets/safer-fetch/model.d.ts +373 -0
- package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/model.js +22 -0
- package/lib/packlets/safer-fetch/model.js.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
- package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
- package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
- package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/redirect.js +98 -0
- package/lib/packlets/safer-fetch/redirect.js.map +1 -0
- package/lib/packlets/safer-fetch/retry.d.ts +162 -0
- package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/retry.js +197 -0
- package/lib/packlets/safer-fetch/retry.js.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
- package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/saferFetch.js +760 -0
- package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
- package/lib/packlets/safer-fetch/transport.d.ts +18 -0
- package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
- package/lib/packlets/safer-fetch/transport.js +53 -0
- package/lib/packlets/safer-fetch/transport.js.map +1 -0
- package/package.json +17 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/deadline.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 { FetchFailureReason, FetchTimeoutPhase } from './failureReason';\n\n/**\n * Why a {@link DeadlineWatch} stopped waiting.\n * @internal\n */\nexport type DeadlineStopCause = 'caller-aborted' | FetchTimeoutPhase;\n\n/**\n * The outcome of racing a promise against the deadlines.\n * @internal\n */\nexport type DeadlineRace<T> =\n | { readonly stopped: false; readonly value: T }\n | { readonly stopped: true; readonly cause: DeadlineStopCause };\n\n/**\n * Composes the caller's cancellation signal with an overall deadline and a headers deadline\n * into one `AbortSignal`, and remembers which of the three stopped the call.\n *\n * @remarks\n * The composition is done with a plain `AbortController` and listeners rather than\n * `AbortSignal.any`, so there is no runtime-version floor to state and no feature-detection\n * branch that only one of the two runtimes would ever execute.\n *\n * The overall deadline covers the connect, the headers, and the body read, and it is what\n * stops a response that dribbles one byte every 25 seconds — such a response passes every\n * per-read check and never trips a connect timeout.\n * @internal\n */\nexport class DeadlineWatch {\n private _controller: AbortController;\n private readonly _startedAt: number;\n private readonly _timeoutMs: number;\n private readonly _headersTimeoutMs: number;\n private readonly _callerSignal: AbortSignal | undefined;\n private readonly _onCallerAbort: () => void;\n\n private _overallTimer: ReturnType<typeof setTimeout> | undefined;\n private _headersTimer: ReturnType<typeof setTimeout> | undefined;\n private _cause: DeadlineStopCause | undefined;\n /**\n * Whether the current stop ends the whole call rather than just this attempt. The overall\n * deadline and the caller's signal are terminal; the per-attempt headers deadline is not.\n */\n private _terminal: boolean;\n private _inBodyPhase: boolean;\n private readonly _waiters: Set<(cause: DeadlineStopCause) => void>;\n\n public constructor(timeoutMs: number, headersTimeoutMs: number, callerSignal?: AbortSignal) {\n this._controller = new AbortController();\n this._startedAt = Date.now();\n this._timeoutMs = timeoutMs;\n this._headersTimeoutMs = headersTimeoutMs;\n this._callerSignal = callerSignal;\n this._terminal = false;\n this._inBodyPhase = false;\n this._waiters = new Set();\n\n this._onCallerAbort = (): void => this._stop('caller-aborted');\n this._overallTimer = setTimeout(() => this._stop(this._inBodyPhase ? 'body' : 'overall'), timeoutMs);\n this._headersTimer = setTimeout(() => this._stop('headers'), headersTimeoutMs);\n\n if (callerSignal !== undefined) {\n if (callerSignal.aborted) {\n this._stop('caller-aborted');\n } else {\n callerSignal.addEventListener('abort', this._onCallerAbort);\n }\n }\n }\n\n /** The composed signal to hand to the transport. */\n public get signal(): AbortSignal {\n return this._controller.signal;\n }\n\n /** Why the call was stopped, or `undefined` while it is still running. */\n public get cause(): DeadlineStopCause | undefined {\n return this._cause;\n }\n\n /**\n * Records that a new attempt is starting: the headers deadline is (re)armed from now.\n *\n * @remarks\n * The headers deadline is **per attempt**, and a redirect walk makes more than one. Without a\n * re-arm, {@link DeadlineWatch.headersReceived} on the first hop would retire the headers\n * deadline for the whole call, leaving every later hop bounded only by the overall deadline —\n * so a chain whose second host simply never answers would hang for the overall budget instead\n * of failing as `timeout.phase === 'headers'`.\n *\n * Called before the address guard rather than before the connect, because guard evaluation\n * (a DNS resolution, in the shipped guard) is part of the time a caller waits for a usable\n * response and is deliberately inside this budget.\n *\n * The overall deadline is untouched: it spans the whole call, redirects included.\n */\n public attemptStarted(): void {\n if (this._cause !== undefined) {\n return;\n }\n if (this._headersTimer !== undefined) {\n clearTimeout(this._headersTimer);\n }\n this._headersTimer = setTimeout(() => this._stop('headers'), this._headersTimeoutMs);\n this._inBodyPhase = false;\n }\n\n /**\n * Records that response headers have arrived: the headers deadline no longer applies to this\n * attempt, and a subsequent overall-deadline expiry is a body-phase timeout rather than an\n * overall one.\n */\n public headersReceived(): void {\n if (this._headersTimer !== undefined) {\n clearTimeout(this._headersTimer);\n this._headersTimer = undefined;\n }\n this._inBodyPhase = true;\n }\n\n /**\n * Races a promise against the deadlines. A stopped race reports the cause; the underlying\n * promise is abandoned, not cancelled — the caller is responsible for releasing whatever\n * resource it represents (a response body reader, in practice).\n *\n * @remarks\n * The waiter is removed once the race settles. A body read calls this once per chunk, so a\n * waiter set that only grew would be a leak proportional to the number of chunks — in the\n * exact code path whose job is to bound what a hostile response can cost the process.\n */\n public async race<T>(promise: Promise<T>): Promise<DeadlineRace<T>> {\n if (this._cause !== undefined) {\n return { stopped: true, cause: this._cause };\n }\n // Definitely assigned: a Promise executor runs synchronously.\n let waiter!: (cause: DeadlineStopCause) => void;\n const stop: Promise<DeadlineRace<T>> = new Promise((resolve) => {\n waiter = (cause: DeadlineStopCause): void => resolve({ stopped: true, cause });\n });\n this._waiters.add(waiter);\n try {\n return await Promise.race([\n promise.then((value): DeadlineRace<T> => ({ stopped: false, value })),\n stop\n ]);\n } finally {\n this._waiters.delete(waiter);\n }\n }\n\n /**\n * Records that an attempt has ended and another may follow: the headers deadline is disarmed,\n * and a stop that ended only *that attempt* is cleared.\n *\n * @remarks\n * **The headers deadline is attempt-scoped and the overall deadline is not.** A retry attempt\n * that timed out waiting for headers must not leave the whole call stopped — without this,\n * retry could never answer a `timeout.phase === 'headers'` failure, which is precisely the\n * failure retry exists for. The composed signal is replaced rather than reset, because an\n * aborted `AbortSignal` cannot be un-aborted.\n *\n * The overall deadline and the caller's signal are **terminal** by contrast: neither is about\n * one attempt, and clearing either would let a retry loop outlive the budget the caller set or\n * ignore the cancellation the caller requested. A terminal stop survives this call, so the\n * backoff that follows is answered immediately and the call ends.\n *\n * Called between attempts rather than at the start of one, so the interval spent in a backoff\n * is bounded by the overall deadline alone — arming a headers deadline over a sleep during\n * which no request is outstanding would stop the call for a response nobody is waiting for.\n */\n public attemptEnded(): void {\n if (this._headersTimer !== undefined) {\n clearTimeout(this._headersTimer);\n this._headersTimer = undefined;\n }\n // No attempt is in flight between attempts, so the call is not in its body phase — whatever\n // the previous attempt reached. Without this, an attempt that failed *after* headers arrived\n // (a body-read network error, a non-2xx) would leave the flag set through the backoff, and\n // an overall-deadline expiry during that sleep would be reported as `timeout.phase: 'body'`\n // while no bytes were being transferred at all. `phase` is part of the taxonomy's contract,\n // and a phase that names the wrong thing is exactly the kind of small lie this primitive is\n // built not to tell.\n this._inBodyPhase = false;\n if (this._terminal || this._cause === undefined) {\n return;\n }\n this._cause = undefined;\n this._controller = new AbortController();\n }\n\n /**\n * How much of the overall deadline is left, in milliseconds, never below zero.\n *\n * @remarks\n * Read by the retry scheduler, which must not sleep past a deadline it is already inside:\n * \"the overall budget is the ceiling\" is only enforceable if the remaining budget is\n * observable.\n */\n public get remainingMs(): number {\n return Math.max(0, this._timeoutMs - (Date.now() - this._startedAt));\n }\n\n /**\n * Waits for the given number of milliseconds, or until the call is stopped — whichever comes\n * first.\n *\n * @remarks\n * Backing the retry delay with the same watch the request races against is what keeps a\n * caller's `abort()` responsive *between* attempts. A bare `setTimeout` would leave a caller\n * who cancelled during a five-second backoff waiting out the full delay before being told the\n * call was aborted.\n *\n * The timer is cleared however the wait ends, so a stopped delay leaves nothing pending —\n * which matters in a test runner, where a stray timer keeps the process alive.\n */\n public async delay(ms: number): Promise<DeadlineRace<true>> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await this.race(\n new Promise<true>((resolve) => {\n timer = setTimeout(() => resolve(true), ms);\n })\n );\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n }\n\n /** Builds the failure reason corresponding to why the call was stopped. */\n public toFailureReason(cause: DeadlineStopCause): FetchFailureReason {\n if (cause === 'caller-aborted') {\n return { kind: 'aborted' };\n }\n return {\n kind: 'timeout',\n phase: cause,\n elapsedMs: Date.now() - this._startedAt,\n limitMs: cause === 'headers' ? this._headersTimeoutMs : this._timeoutMs\n };\n }\n\n /** Clears timers and listeners. Safe to call more than once. */\n public dispose(): void {\n if (this._overallTimer !== undefined) {\n clearTimeout(this._overallTimer);\n this._overallTimer = undefined;\n }\n if (this._headersTimer !== undefined) {\n clearTimeout(this._headersTimer);\n this._headersTimer = undefined;\n }\n if (this._callerSignal !== undefined) {\n this._callerSignal.removeEventListener('abort', this._onCallerAbort);\n }\n this._waiters.clear();\n }\n\n private _stop(cause: DeadlineStopCause): void {\n // First cause wins. The overall and headers deadlines can be scheduled for the same instant,\n // and a caller can abort while one of them is already firing; reporting the second would\n // rewrite a phase the caller has arguably already been told about.\n if (this._cause !== undefined) {\n return;\n }\n this._cause = cause;\n // Every cause but the per-attempt headers deadline ends the call: the overall deadline is\n // the whole budget, and the caller's signal is the caller's decision.\n this._terminal = cause !== 'headers';\n const waiters = Array.from(this._waiters);\n this._waiters.clear();\n this._controller.abort();\n for (const waiter of waiters) {\n waiter(cause);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
* Default overall deadline, in milliseconds — comfortably above a slow-but-real API and far
|
|
22
|
+
* below "a hung request pins a socket for minutes". Streaming LLM calls should not use this
|
|
23
|
+
* primitive at all; that is what `AiAssist` is for.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
27
|
+
/**
|
|
28
|
+
* Default deadline to first response headers, in milliseconds. A host that has not sent
|
|
29
|
+
* headers in ten seconds is not answering.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_HEADERS_TIMEOUT_MS = 10000;
|
|
33
|
+
/**
|
|
34
|
+
* Default cap on decoded response bytes (5 MiB) — roughly an order of magnitude above a
|
|
35
|
+
* realistic JSON API response and well below a size that threatens a Node heap.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Raising it is a per-call option, not a construction-time setting, precisely because real
|
|
39
|
+
* documents clear this. See `ISaferFetchOptions.maxResponseBytes`.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
43
|
+
/**
|
|
44
|
+
* Redirect statuses this primitive recognizes as redirects. Other 3xx statuses (`300`, `304`,
|
|
45
|
+
* …) do not direct the client at a new URL and are reported as ordinary non-2xx responses.
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export const REDIRECT_STATUSES = [301, 302, 303, 307, 308];
|
|
49
|
+
/**
|
|
50
|
+
* Default cap on redirect hops followed under `'validate-each-hop'`.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Five is enough for the ordinary shortener-then-canonicalize-then-CDN chains real services
|
|
54
|
+
* produce and small enough that a chain designed to burn budget is refused quickly. Every hop
|
|
55
|
+
* costs a full guard evaluation — including a DNS resolution — against the same overall
|
|
56
|
+
* deadline, so the cap bounds work, not just politeness.
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_MAX_REDIRECTS = 5;
|
|
60
|
+
/**
|
|
61
|
+
* Header names dropped on every cross-origin redirect hop, whatever the caller configured.
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* Turning on manual redirects makes this primitive responsible for a rule the platform was
|
|
65
|
+
* applying for free: browsers and `curl` both strip credential headers when a redirect leaves
|
|
66
|
+
* the origin, and a hand-rolled loop that replays them hands `Authorization: Bearer …` to
|
|
67
|
+
* whatever host the redirect names. Callers add their own names with
|
|
68
|
+
* `ISaferFetchOptions.sensitiveHeaders`; these three are not removable.
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export const ALWAYS_STRIPPED_HEADERS = [
|
|
72
|
+
'authorization',
|
|
73
|
+
'cookie',
|
|
74
|
+
'proxy-authorization'
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* URL schemes this primitive will ever request.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Core rejects everything else — `file:`, `data:`, `blob:`, `ftp:`, `gopher:`, `ws:` — outright
|
|
81
|
+
* and at every hop, because none of them is a legitimate network fetch and each is a standard
|
|
82
|
+
* SSRF payload. Deciding between `http:` and `https:`, and which ports are acceptable, belongs
|
|
83
|
+
* to the address guard, which is why core does not narrow this further.
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
86
|
+
export const SUPPORTED_SCHEMES = ['http:', 'https:'];
|
|
87
|
+
/**
|
|
88
|
+
* Default base delay for {@link SaferFetch.IRetryPolicy | retry} backoff, in milliseconds.
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export const DEFAULT_RETRY_BASE_DELAY_MS = 250;
|
|
92
|
+
/**
|
|
93
|
+
* Default ceiling on a single {@link SaferFetch.IRetryPolicy | retry} delay, in milliseconds.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* It bounds the computed backoff **and** a server-supplied `Retry-After`. That second role is
|
|
97
|
+
* the security-relevant one: `Retry-After` is a header an attacker-controlled server chooses
|
|
98
|
+
* freely, so an unclamped `Retry-After: 86400` is a denial of service on the caller.
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
export const DEFAULT_RETRY_MAX_DELAY_MS = 5000;
|
|
102
|
+
/**
|
|
103
|
+
* HTTP statuses a {@link SaferFetch.IRetryPolicy | retry policy} will retry.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Every other status — including every other 4xx — is terminal. A `401`, a `403` and a `404`
|
|
107
|
+
* mean the same thing on the second attempt as on the first, and retrying them turns a client
|
|
108
|
+
* bug into load against a service that already said no.
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export const RETRYABLE_HTTP_STATUSES = [408, 429, 500, 502, 503, 504];
|
|
112
|
+
/**
|
|
113
|
+
* Statuses whose `Retry-After` header is honored.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* `429` and `503` are the two the header is specified for and the two where it carries real
|
|
117
|
+
* scheduling information. Honoring it on a `500` would let any failing endpoint dictate the
|
|
118
|
+
* caller's schedule for no benefit.
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
export const RETRY_AFTER_STATUSES = [429, 503];
|
|
122
|
+
/**
|
|
123
|
+
* Methods retried without an explicit opt-in.
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* A timeout does not tell you whether the server processed the request, so a retried `POST`
|
|
127
|
+
* can double-charge. `retryNonIdempotent` opts out, per call, visibly.
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
export const IDEMPOTENT_METHODS = ['GET', 'HEAD'];
|
|
131
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/defaults.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;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAW,KAAM,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAW,KAAM,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAW,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAElE;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAElF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAW,CAAC,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA0B;IAC5D,eAAe;IACf,QAAQ;IACR,qBAAqB;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAE5E;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAW,GAAG,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAW,IAAK,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE7F;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA0B,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAEtE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA0B,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC","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 * Default overall deadline, in milliseconds — comfortably above a slow-but-real API and far\n * below \"a hung request pins a socket for minutes\". Streaming LLM calls should not use this\n * primitive at all; that is what `AiAssist` is for.\n * @public\n */\nexport const DEFAULT_TIMEOUT_MS: number = 30_000;\n\n/**\n * Default deadline to first response headers, in milliseconds. A host that has not sent\n * headers in ten seconds is not answering.\n * @public\n */\nexport const DEFAULT_HEADERS_TIMEOUT_MS: number = 10_000;\n\n/**\n * Default cap on decoded response bytes (5 MiB) — roughly an order of magnitude above a\n * realistic JSON API response and well below a size that threatens a Node heap.\n *\n * @remarks\n * Raising it is a per-call option, not a construction-time setting, precisely because real\n * documents clear this. See `ISaferFetchOptions.maxResponseBytes`.\n * @public\n */\nexport const DEFAULT_MAX_RESPONSE_BYTES: number = 5 * 1024 * 1024;\n\n/**\n * Redirect statuses this primitive recognizes as redirects. Other 3xx statuses (`300`, `304`,\n * …) do not direct the client at a new URL and are reported as ordinary non-2xx responses.\n * @public\n */\nexport const REDIRECT_STATUSES: ReadonlyArray<number> = [301, 302, 303, 307, 308];\n\n/**\n * Default cap on redirect hops followed under `'validate-each-hop'`.\n *\n * @remarks\n * Five is enough for the ordinary shortener-then-canonicalize-then-CDN chains real services\n * produce and small enough that a chain designed to burn budget is refused quickly. Every hop\n * costs a full guard evaluation — including a DNS resolution — against the same overall\n * deadline, so the cap bounds work, not just politeness.\n * @public\n */\nexport const DEFAULT_MAX_REDIRECTS: number = 5;\n\n/**\n * Header names dropped on every cross-origin redirect hop, whatever the caller configured.\n *\n * @remarks\n * Turning on manual redirects makes this primitive responsible for a rule the platform was\n * applying for free: browsers and `curl` both strip credential headers when a redirect leaves\n * the origin, and a hand-rolled loop that replays them hands `Authorization: Bearer …` to\n * whatever host the redirect names. Callers add their own names with\n * `ISaferFetchOptions.sensitiveHeaders`; these three are not removable.\n * @public\n */\nexport const ALWAYS_STRIPPED_HEADERS: ReadonlyArray<string> = [\n 'authorization',\n 'cookie',\n 'proxy-authorization'\n];\n\n/**\n * URL schemes this primitive will ever request.\n *\n * @remarks\n * Core rejects everything else — `file:`, `data:`, `blob:`, `ftp:`, `gopher:`, `ws:` — outright\n * and at every hop, because none of them is a legitimate network fetch and each is a standard\n * SSRF payload. Deciding between `http:` and `https:`, and which ports are acceptable, belongs\n * to the address guard, which is why core does not narrow this further.\n * @public\n */\nexport const SUPPORTED_SCHEMES: ReadonlyArray<string> = ['http:', 'https:'];\n\n/**\n * Default base delay for {@link SaferFetch.IRetryPolicy | retry} backoff, in milliseconds.\n * @public\n */\nexport const DEFAULT_RETRY_BASE_DELAY_MS: number = 250;\n\n/**\n * Default ceiling on a single {@link SaferFetch.IRetryPolicy | retry} delay, in milliseconds.\n *\n * @remarks\n * It bounds the computed backoff **and** a server-supplied `Retry-After`. That second role is\n * the security-relevant one: `Retry-After` is a header an attacker-controlled server chooses\n * freely, so an unclamped `Retry-After: 86400` is a denial of service on the caller.\n * @public\n */\nexport const DEFAULT_RETRY_MAX_DELAY_MS: number = 5_000;\n\n/**\n * HTTP statuses a {@link SaferFetch.IRetryPolicy | retry policy} will retry.\n *\n * @remarks\n * Every other status — including every other 4xx — is terminal. A `401`, a `403` and a `404`\n * mean the same thing on the second attempt as on the first, and retrying them turns a client\n * bug into load against a service that already said no.\n * @public\n */\nexport const RETRYABLE_HTTP_STATUSES: ReadonlyArray<number> = [408, 429, 500, 502, 503, 504];\n\n/**\n * Statuses whose `Retry-After` header is honored.\n *\n * @remarks\n * `429` and `503` are the two the header is specified for and the two where it carries real\n * scheduling information. Honoring it on a `500` would let any failing endpoint dictate the\n * caller's schedule for no benefit.\n * @public\n */\nexport const RETRY_AFTER_STATUSES: ReadonlyArray<number> = [429, 503];\n\n/**\n * Methods retried without an explicit opt-in.\n *\n * @remarks\n * A timeout does not tell you whether the server processed the request, so a retried `POST`\n * can double-charge. `retryNonIdempotent` opts out, per call, visibly.\n * @public\n */\nexport const IDEMPOTENT_METHODS: ReadonlyArray<string> = ['GET', 'HEAD'];\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
export {};
|
|
21
|
+
//# 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,148 @@
|
|
|
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 { mediaTypeMatches, parseMediaType, parseMediaTypePattern } from './contentType';
|
|
22
|
+
/**
|
|
23
|
+
* An address guard that permits every address.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* **This is the absence of the SSRF guarantee, given a name.** It performs no address
|
|
27
|
+
* classification, no DNS resolution, and no scheme or port narrowing beyond the core's
|
|
28
|
+
* refusal of non-`http(s)` schemes. A call site using it is reachable at any address the
|
|
29
|
+
* process can route to, including cloud metadata endpoints, loopback admin ports, and
|
|
30
|
+
* RFC-1918 hosts.
|
|
31
|
+
*
|
|
32
|
+
* It ships anyway, for two reasons. It is the honest and only possible choice in a browser,
|
|
33
|
+
* where neither DNS resolution nor redirect interposition exists. And omitting it would not
|
|
34
|
+
* make anyone safer — it would make consumers hand-roll something worse. It is deliberately
|
|
35
|
+
* named to be uncomfortable in review and to grep as a distinct posture.
|
|
36
|
+
*
|
|
37
|
+
* This is the **guard-layer** factory: it returns an {@link SaferFetch.IAddressGuard}, which is
|
|
38
|
+
* what an entry point's `addressGuard` option takes. A pure address *policy* — the synchronous,
|
|
39
|
+
* address-list-in classifier a resolving guard delegates to — is a different layer and needs a
|
|
40
|
+
* distinct name.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export function allowAnyAddress() {
|
|
44
|
+
return {
|
|
45
|
+
name: 'allowAnyAddress',
|
|
46
|
+
check: async (chain) => {
|
|
47
|
+
var _a;
|
|
48
|
+
const hop = (_a = chain[chain.length - 1]) !== null && _a !== void 0 ? _a : undefined;
|
|
49
|
+
if (hop === undefined) {
|
|
50
|
+
return fail('allowAnyAddress: hop chain is empty.');
|
|
51
|
+
}
|
|
52
|
+
// pinnedAddress is deliberately left undefined: this guard validates nothing, so it has
|
|
53
|
+
// no address to pin, and a transport must never be told otherwise.
|
|
54
|
+
return succeed({ url: hop.url });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A response-headers guard that accepts only the listed media types.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Rejecting on `Content-Type` is strictly cheaper than capping mid-read — a header comparison
|
|
63
|
+
* instead of a partial body transfer — so a consumer ingesting URLs wants it on every call. It
|
|
64
|
+
* is also what keeps an HTML error page served with a `200` from being parsed as JSON twenty
|
|
65
|
+
* frames away from where it went wrong.
|
|
66
|
+
*
|
|
67
|
+
* Entries are media types, optionally with a wildcard subtype (`text/*`); a wildcard type
|
|
68
|
+
* requires a wildcard subtype. Matching is case-insensitive and ignores parameters, so
|
|
69
|
+
* `text/html` accepts `text/html; charset=utf-8`. A response with **no** `Content-Type` is
|
|
70
|
+
* rejected: an untyped response has not satisfied a content-type allowlist.
|
|
71
|
+
*
|
|
72
|
+
* Construction is fallible because a malformed entry in a security-adjacent allowlist must be
|
|
73
|
+
* reported to its author rather than silently compiled into a pattern that never matches.
|
|
74
|
+
*
|
|
75
|
+
* @param types - Accepted media types. Must be non-empty.
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export function allowContentTypes(types) {
|
|
79
|
+
if (types.length === 0) {
|
|
80
|
+
return fail('allowContentTypes: at least one media type is required.');
|
|
81
|
+
}
|
|
82
|
+
return mapResults(types.map((t) => parseMediaTypePattern(t)))
|
|
83
|
+
.withErrorFormat((message) => `allowContentTypes: ${message}`)
|
|
84
|
+
.onSuccess((patterns) => {
|
|
85
|
+
const accepted = patterns.map((p) => `${p.type}/${p.subtype}`);
|
|
86
|
+
return succeed({
|
|
87
|
+
name: 'allowContentTypes',
|
|
88
|
+
acceptedContentTypes: accepted,
|
|
89
|
+
check: async (head) => {
|
|
90
|
+
var _a;
|
|
91
|
+
const contentType = (_a = head.contentType) !== null && _a !== void 0 ? _a : undefined;
|
|
92
|
+
if (contentType === undefined) {
|
|
93
|
+
return fail(`response has no content-type; expected one of [${accepted.join(', ')}].`);
|
|
94
|
+
}
|
|
95
|
+
return parseMediaType(contentType)
|
|
96
|
+
.withErrorFormat((message) => `content-type ${message}`)
|
|
97
|
+
.onSuccess((actual) => {
|
|
98
|
+
if (patterns.some((p) => mediaTypeMatches(actual, p))) {
|
|
99
|
+
return succeed(true);
|
|
100
|
+
}
|
|
101
|
+
return fail(`content-type "${contentType}" is not accepted; expected one of [${accepted.join(', ')}].`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const passthroughRequestGuard = {
|
|
108
|
+
name: 'passthrough',
|
|
109
|
+
check: async (request) => succeed(request)
|
|
110
|
+
};
|
|
111
|
+
const passthroughResponseHeadersGuard = {
|
|
112
|
+
name: 'passthrough',
|
|
113
|
+
check: async () => succeed(true)
|
|
114
|
+
};
|
|
115
|
+
const passthroughResponseBodyGuard = {
|
|
116
|
+
name: 'passthrough',
|
|
117
|
+
check: async () => succeed(true)
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Resolves the guards a call will use, applying defaults once at the boundary so that no
|
|
121
|
+
* downstream code path branches on a guard's absence.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* Every optional guard resolves with `??`, uniformly. That uniformity is the point: a
|
|
125
|
+
* `!== undefined` test in one slot where its siblings use `??` lets a `null` — from a
|
|
126
|
+
* JavaScript caller, or through an `unknown` escape hatch — pass straight through and install
|
|
127
|
+
* itself as a guard, which is a real defect that has shipped in this repo before. A `null`
|
|
128
|
+
* guard here is treated as *absent*, never as *installed*.
|
|
129
|
+
*
|
|
130
|
+
* `addressGuard` has no default, so a `null` or missing value is a hard failure rather than a
|
|
131
|
+
* silent passthrough.
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
export function resolveGuards(options) {
|
|
135
|
+
var _a, _b, _c, _d;
|
|
136
|
+
const address = (_a = options.addressGuard) !== null && _a !== void 0 ? _a : undefined;
|
|
137
|
+
if (address === undefined) {
|
|
138
|
+
return fail('addressGuard is required and has no default. Supply a named factory — ' +
|
|
139
|
+
'allowAnyAddress() is the explicit opt-out.');
|
|
140
|
+
}
|
|
141
|
+
return succeed({
|
|
142
|
+
address,
|
|
143
|
+
request: (_b = options.requestGuard) !== null && _b !== void 0 ? _b : passthroughRequestGuard,
|
|
144
|
+
responseHeaders: (_c = options.responseHeadersGuard) !== null && _c !== void 0 ? _c : passthroughResponseHeadersGuard,
|
|
145
|
+
responseBody: (_d = options.responseBodyGuard) !== null && _d !== void 0 ? _d : passthroughResponseBodyGuard
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/guards.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,OAAO,EAAE,IAAI,EAAE,UAAU,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAc,gBAAgB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAcpG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE,KAAK,EAAE,KAAiC,EAAkC,EAAE;;YACjF,MAAM,GAAG,GAAG,MAAA,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,mCAAI,SAAS,CAAC;YACjD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBACtB,OAAO,IAAI,CAAC,sCAAsC,CAAC,CAAC;YACtD,CAAC;YACD,wFAAwF;YACxF,mEAAmE;YACnE,OAAO,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA4B;IAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,yDAAyD,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC;SAC1D,eAAe,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,sBAAsB,OAAO,EAAE,CAAC;SAC7D,SAAS,CAAC,CAAC,QAAsB,EAAE,EAAE;QACpC,MAAM,QAAQ,GAA0B,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,OAAO,OAAO,CAAC;YACb,IAAI,EAAE,mBAAmB;YACzB,oBAAoB,EAAE,QAAQ;YAC9B,KAAK,EAAE,KAAK,EAAE,IAA6B,EAAyB,EAAE;;gBACpE,MAAM,WAAW,GAAG,MAAA,IAAI,CAAC,WAAW,mCAAI,SAAS,CAAC;gBAClD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC9B,OAAO,IAAI,CAAC,kDAAkD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACzF,CAAC;gBACD,OAAO,cAAc,CAAC,WAAW,CAAC;qBAC/B,eAAe,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,OAAO,EAAE,CAAC;qBACvD,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;oBACpB,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBACtD,OAAO,OAAO,CAAC,IAAa,CAAC,CAAC;oBAChC,CAAC;oBACD,OAAO,IAAI,CACT,iBAAiB,WAAW,uCAAuC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC3F,CAAC;gBACJ,CAAC,CAAC,CAAC;YACP,CAAC;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC;AAED,MAAM,uBAAuB,GAAkB;IAC7C,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,KAAK,EAAE,OAA2B,EAAuC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;CACpG,CAAC;AAEF,MAAM,+BAA+B,GAA0B;IAC7D,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,KAAK,IAA2B,EAAE,CAAC,OAAO,CAAC,IAAa,CAAC;CACjE,CAAC;AAEF,MAAM,4BAA4B,GAAuB;IACvD,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,KAAK,IAA2B,EAAE,CAAC,OAAO,CAAC,IAAa,CAAC;CACjE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,OAA2B;;IACvD,MAAM,OAAO,GAAG,MAAA,OAAO,CAAC,YAAY,mCAAI,SAAS,CAAC;IAClD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,IAAI,CACT,wEAAwE;YACtE,4CAA4C,CAC/C,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;QACb,OAAO;QACP,OAAO,EAAE,MAAA,OAAO,CAAC,YAAY,mCAAI,uBAAuB;QACxD,eAAe,EAAE,MAAA,OAAO,CAAC,oBAAoB,mCAAI,+BAA+B;QAChF,YAAY,EAAE,MAAA,OAAO,CAAC,iBAAiB,mCAAI,4BAA4B;KACxE,CAAC,CAAC;AACL,CAAC","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,58 @@
|
|
|
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
|
+
* An HTTP fetch primitive with an explicit threat model (browser version).
|
|
22
|
+
*
|
|
23
|
+
* This barrel deliberately omits `blockPrivateNetworks` and the resolver seam beneath it: that
|
|
24
|
+
* module imports `node:dns/promises`, and exporting it here would pull a Node builtin into a
|
|
25
|
+
* browser bundle.
|
|
26
|
+
*
|
|
27
|
+
* The address-classification layer — `classifyAddress`, and the pure synchronous policies
|
|
28
|
+
* `allowAnyAddressPolicy` / `blockPrivateNetworksPolicy` — **is** exported here: it is pure
|
|
29
|
+
* arithmetic over parsed octets, it works identically in a browser, and it is useful for a
|
|
30
|
+
* URL-zero check. It is not a substitute for the guard, and cannot be: it classifies an address
|
|
31
|
+
* a caller already holds, while what the browser lacks is any way to learn the address a
|
|
32
|
+
* hostname resolves to.
|
|
33
|
+
*
|
|
34
|
+
* Note: the resolved-address (private-IP) guard and per-hop redirect revalidation are NOT
|
|
35
|
+
* available in a browser, and not for want of implementation. There is no browser API that
|
|
36
|
+
* returns a hostname's A/AAAA records, nothing in `fetch` or `Response` exposes the peer
|
|
37
|
+
* address, and `redirect: 'manual'` yields an opaque response whose `Location` is not
|
|
38
|
+
* readable — a `'validate-each-hop'` call there fails as `'redirect-opaque'` rather than
|
|
39
|
+
* quietly following anything. `allowAnyAddress()` is the honest choice, and its name says so.
|
|
40
|
+
*
|
|
41
|
+
* @packageDocumentation
|
|
42
|
+
*/
|
|
43
|
+
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';
|
|
44
|
+
export { allowAnyAddress, allowContentTypes } from './guards';
|
|
45
|
+
export { platformFetchTransport } from './transport';
|
|
46
|
+
export { saferFetchBytes, saferFetchJson, saferFetchText } from './saferFetch';
|
|
47
|
+
// Runtime-agnostic and exported from both barrels: the address *classification* layer is pure
|
|
48
|
+
// arithmetic over parsed octets with no I/O, so it works in a browser exactly as it does on
|
|
49
|
+
// Node. It is useful there for a URL-zero check — a caller can refuse an IP-literal URL that
|
|
50
|
+
// classifies as private before ever calling `fetch`.
|
|
51
|
+
//
|
|
52
|
+
// **It cannot substitute for the resolved-address guard.** It classifies an address you already
|
|
53
|
+
// have; the guarantee the browser lacks is *obtaining* the address a hostname resolves to, which
|
|
54
|
+
// no browser API provides. `https://internal.example.com/` resolving to `10.0.0.5` is invisible
|
|
55
|
+
// to every function exported here.
|
|
56
|
+
export { allowAnyAddressPolicy, blockPrivateNetworksPolicy } from './addressPolicy';
|
|
57
|
+
export { classifyAddress } from './addressClassification';
|
|
58
|
+
//# 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,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;AAuBpB,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,EAA+B,MAAM,cAAc,CAAC;AAE5G,8FAA8F;AAC9F,4FAA4F;AAC5F,6FAA6F;AAC7F,qDAAqD;AACrD,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,gGAAgG;AAChG,mCAAmC;AACnC,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAI3B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,eAAe,EAMhB,MAAM,yBAAyB,CAAC","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,52 @@
|
|
|
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
|
+
* A safer `fetch` primitive with an explicit threat model.
|
|
22
|
+
*
|
|
23
|
+
* This is deliberately **not** a thin, unopinionated boundary over an upstream library. There
|
|
24
|
+
* is no upstream to wrap — `fetch` is a platform global — and the opinion *is* the product:
|
|
25
|
+
* the deadlines, the scheme refusal, the streaming size cap, the redirect posture, and the
|
|
26
|
+
* required address guard are the deliverable. A caller who strips the opinion out has `fetch`,
|
|
27
|
+
* which is where they started.
|
|
28
|
+
*
|
|
29
|
+
* The guiding constraint: *a primitive that advertises a guarantee it does not have is worse
|
|
30
|
+
* than five lines at a call site, because it transfers responsibility without transferring
|
|
31
|
+
* protection.* Every entry point's documentation names what it does **not** protect against
|
|
32
|
+
* next to what it does.
|
|
33
|
+
*
|
|
34
|
+
* @packageDocumentation
|
|
35
|
+
*/
|
|
36
|
+
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';
|
|
37
|
+
export { allowAnyAddress, allowContentTypes } from './guards';
|
|
38
|
+
export { platformFetchTransport } from './transport';
|
|
39
|
+
export { saferFetchBytes, saferFetchJson, saferFetchText } from './saferFetch';
|
|
40
|
+
// Two layers, one naming rule: an **unsuffixed** factory returns the asynchronous,
|
|
41
|
+
// hop-chain-aware, name-resolving `IAddressGuard` that an entry point's `addressGuard` option
|
|
42
|
+
// takes (`allowAnyAddress`, `blockPrivateNetworks`), while a **`Policy`-suffixed** factory
|
|
43
|
+
// returns the pure, synchronous `IAddressPolicy` over an already-resolved address list that such
|
|
44
|
+
// a guard delegates to (`allowAnyAddressPolicy`, `blockPrivateNetworksPolicy`). The unsuffixed
|
|
45
|
+
// names are the ones callers reach for, which is why they are the ones that fit the call site.
|
|
46
|
+
export { allowAnyAddressPolicy, blockPrivateNetworksPolicy } from './addressPolicy';
|
|
47
|
+
// Node only — it resolves names, and no browser API returns a hostname's A/AAAA records. This
|
|
48
|
+
// is the one module in the packlet that performs I/O, and the reason this barrel and the browser
|
|
49
|
+
// barrel differ at all.
|
|
50
|
+
export { blockPrivateNetworks, nodeHostResolver } from './nodeAddressGuard';
|
|
51
|
+
export { classifyAddress } from './addressClassification';
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/safer-fetch/index.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;;;;;;;;;;;;;;;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;AAuBpB,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,EAA+B,MAAM,cAAc,CAAC;AAE5G,mFAAmF;AACnF,8FAA8F;AAC9F,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,+FAA+F;AAC/F,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAI3B,MAAM,iBAAiB,CAAC;AAEzB,8FAA8F;AAC9F,iGAAiG;AACjG,wBAAwB;AACxB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAGjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,eAAe,EAMhB,MAAM,yBAAyB,CAAC","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 * A safer `fetch` primitive with an explicit threat model.\n *\n * This is deliberately **not** a thin, unopinionated boundary over an upstream library. There\n * is no upstream to wrap — `fetch` is a platform global — and the opinion *is* the product:\n * the deadlines, the scheme refusal, the streaming size cap, the redirect posture, and the\n * required address guard are the deliverable. A caller who strips the opinion out has `fetch`,\n * which is where they started.\n *\n * The guiding constraint: *a primitive that advertises a guarantee it does not have is worse\n * than five lines at a call site, because it transfers responsibility without transferring\n * protection.* Every entry point's documentation names what it does **not** protect against\n * next to what it does.\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// Two layers, one naming rule: an **unsuffixed** factory returns the asynchronous,\n// hop-chain-aware, name-resolving `IAddressGuard` that an entry point's `addressGuard` option\n// takes (`allowAnyAddress`, `blockPrivateNetworks`), while a **`Policy`-suffixed** factory\n// returns the pure, synchronous `IAddressPolicy` over an already-resolved address list that such\n// a guard delegates to (`allowAnyAddressPolicy`, `blockPrivateNetworksPolicy`). The unsuffixed\n// names are the ones callers reach for, which is why they are the ones that fit the call site.\nexport {\n allowAnyAddressPolicy,\n blockPrivateNetworksPolicy,\n type IAddressCheckVerdict,\n type IAddressPolicy,\n type IBlockPrivateNetworksOptions\n} from './addressPolicy';\n\n// Node only — it resolves names, and no browser API returns a hostname's A/AAAA records. This\n// is the one module in the packlet that performs I/O, and the reason this barrel and the browser\n// barrel differ at all.\nexport {\n blockPrivateNetworks,\n nodeHostResolver,\n type HostResolver,\n type IBlockPrivateNetworksGuardOptions\n} from './nodeAddressGuard';\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,21 @@
|
|
|
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
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=model.js.map
|