lifecycleion 0.0.20 → 0.0.21

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 (72) hide show
  1. package/README.md +39 -36
  2. package/dist/lib/event-emitter.cjs +195 -9
  3. package/dist/lib/event-emitter.cjs.map +1 -1
  4. package/dist/lib/event-emitter.js +195 -9
  5. package/dist/lib/event-emitter.js.map +1 -1
  6. package/dist/lib/global-event-target.cjs +217 -0
  7. package/dist/lib/global-event-target.cjs.map +1 -0
  8. package/dist/lib/global-event-target.d.cts +70 -0
  9. package/dist/lib/global-event-target.d.ts +70 -0
  10. package/dist/lib/global-event-target.js +189 -0
  11. package/dist/lib/global-event-target.js.map +1 -0
  12. package/dist/lib/http-client/index.cjs +554 -193
  13. package/dist/lib/http-client/index.cjs.map +1 -1
  14. package/dist/lib/http-client/index.d.cts +9 -3
  15. package/dist/lib/http-client/index.d.ts +9 -3
  16. package/dist/lib/http-client/index.js +554 -193
  17. package/dist/lib/http-client/index.js.map +1 -1
  18. package/dist/lib/http-client-mock/index.cjs +71 -10
  19. package/dist/lib/http-client-mock/index.cjs.map +1 -1
  20. package/dist/lib/http-client-mock/index.d.cts +50 -3
  21. package/dist/lib/http-client-mock/index.d.ts +50 -3
  22. package/dist/lib/http-client-mock/index.js +71 -10
  23. package/dist/lib/http-client-mock/index.js.map +1 -1
  24. package/dist/lib/http-client-node/index.cjs +98 -19
  25. package/dist/lib/http-client-node/index.cjs.map +1 -1
  26. package/dist/lib/http-client-node/index.d.cts +2 -2
  27. package/dist/lib/http-client-node/index.d.ts +2 -2
  28. package/dist/lib/http-client-node/index.js +98 -19
  29. package/dist/lib/http-client-node/index.js.map +1 -1
  30. package/dist/lib/http-client-xhr/index.cjs.map +1 -1
  31. package/dist/lib/http-client-xhr/index.d.cts +2 -2
  32. package/dist/lib/http-client-xhr/index.d.ts +2 -2
  33. package/dist/lib/http-client-xhr/index.js.map +1 -1
  34. package/dist/lib/lifecycle-manager/index.cjs +199 -13
  35. package/dist/lib/lifecycle-manager/index.cjs.map +1 -1
  36. package/dist/lib/lifecycle-manager/index.js +199 -13
  37. package/dist/lib/lifecycle-manager/index.js.map +1 -1
  38. package/dist/lib/logger/index.cjs +203 -21
  39. package/dist/lib/logger/index.cjs.map +1 -1
  40. package/dist/lib/logger/index.d.cts +5 -1
  41. package/dist/lib/logger/index.d.ts +5 -1
  42. package/dist/lib/logger/index.js +203 -21
  43. package/dist/lib/logger/index.js.map +1 -1
  44. package/dist/lib/lru-cache/index.cjs +195 -9
  45. package/dist/lib/lru-cache/index.cjs.map +1 -1
  46. package/dist/lib/lru-cache/index.js +195 -9
  47. package/dist/lib/lru-cache/index.js.map +1 -1
  48. package/dist/lib/process-signal-manager.cjs +199 -13
  49. package/dist/lib/process-signal-manager.cjs.map +1 -1
  50. package/dist/lib/process-signal-manager.js +199 -13
  51. package/dist/lib/process-signal-manager.js.map +1 -1
  52. package/dist/lib/promise-protected-resolver.cjs +195 -9
  53. package/dist/lib/promise-protected-resolver.cjs.map +1 -1
  54. package/dist/lib/promise-protected-resolver.js +195 -9
  55. package/dist/lib/promise-protected-resolver.js.map +1 -1
  56. package/dist/lib/retry-utils/index.cjs +195 -9
  57. package/dist/lib/retry-utils/index.cjs.map +1 -1
  58. package/dist/lib/retry-utils/index.js +195 -9
  59. package/dist/lib/retry-utils/index.js.map +1 -1
  60. package/dist/lib/safe-handle-callback.cjs +196 -18
  61. package/dist/lib/safe-handle-callback.cjs.map +1 -1
  62. package/dist/lib/safe-handle-callback.d.cts +14 -4
  63. package/dist/lib/safe-handle-callback.d.ts +14 -4
  64. package/dist/lib/safe-handle-callback.js +196 -18
  65. package/dist/lib/safe-handle-callback.js.map +1 -1
  66. package/dist/lib/single-event-observer.cjs +195 -9
  67. package/dist/lib/single-event-observer.cjs.map +1 -1
  68. package/dist/lib/single-event-observer.js +195 -9
  69. package/dist/lib/single-event-observer.js.map +1 -1
  70. package/dist/{types-6G59m8U9.d.cts → types-BFvpJPMt.d.cts} +71 -10
  71. package/dist/{types-CG10j6Lc.d.ts → types-BONtIhQ5.d.ts} +71 -10
  72. package/package.json +12 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/http-client-xhr/index.ts","../../../src/lib/http-client/consts.ts","../../../src/lib/http-client/utils.ts","../../../src/lib/http-client/adapters/xhr-adapter.ts"],"sourcesContent":["export { XHRAdapter } from '../http-client/adapters/xhr-adapter';\n\n/**\n * Adapter contract types, re-exported so code driving an adapter directly —\n * building AdapterRequest objects and calling send() rather than going through\n * HTTPClient — does not have to import types from 'lifecycleion/http-client',\n * the module it is specifically not using. Type-only, so nothing is added to\n * the runtime bundle.\n */\nexport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n AdapterProgressEvent,\n HTTPMethod,\n} from '../http-client/types';\n","import type { HTTPMethod } from './types';\n\n/**\n * HTTP responses that are plausibly transient and worth retrying when a retry\n * policy is explicitly enabled.\n *\n * `status === 0` is included on purpose because browser/XHR-style adapters can\n * surface \"no real HTTP response\" that way when the network is unavailable or\n * the request otherwise fails before a normal status code is received.\n */\nexport const RETRYABLE_STATUS_CODES: ReadonlySet<number> = new Set([\n // 0: Browser/XHR-style \"no response\" status.\n 0,\n\n // 408 Request Timeout\n 408,\n // 429 Too Many Requests\n 429,\n\n // 500 Internal Server Error\n 500,\n // 502 Bad Gateway\n 502,\n // 503 Service Unavailable\n 503,\n // 504 Gateway Timeout\n 504,\n\n // 507 Insufficient Storage\n 507,\n // 509 Bandwidth Limit Exceeded (non-standard)\n 509,\n // 520 Unknown Error (Cloudflare)\n 520,\n // 521 Web Server Is Down (Cloudflare)\n 521,\n // 522 Connection Timed Out (Cloudflare)\n 522,\n // 523 Origin Is Unreachable (Cloudflare)\n 523,\n // 524 A Timeout Occurred (Cloudflare)\n 524,\n // 598 Network Read Timeout Error (non-standard)\n 598,\n // 599 Network Connect Timeout Error (non-standard)\n 599,\n]);\n\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-local-client-request-id';\n\nexport const DEFAULT_REQUEST_ATTEMPT_HEADER = 'x-local-client-request-attempt';\n\nexport const DEFAULT_USER_AGENT = 'lifecycleion-http-client';\n\nexport const NON_RETRYABLE_HTTP_CLIENT_CALLBACK_ERROR_FLAG =\n '_lifecycleion_non_retryable_http_client_callback_error';\n\nexport const STREAM_FACTORY_ERROR_FLAG = '_lifecycleion_stream_factory_error';\n\n/**\n * Attached to the AbortError thrown when a StreamResponseFactory returns null\n * or `{ cancel: true, reason? }`. The value is the reason string if provided,\n * or `true` if the factory cancelled without a reason. Lets HTTPClient surface\n * the reason on HTTPClientError.cancelReason.\n */\nexport const STREAM_FACTORY_CANCEL_KEY =\n '_lifecycleion_stream_factory_cancel_reason';\n\nexport const RESPONSE_STREAM_ABORT_FLAG = '_lifecycleion_response_stream_abort';\n\n/**\n * Set on the AbortError thrown by XHRAdapter's defensive `timeout` event\n * listener. Lets HTTPClient classify the error as a timeout (retryable) rather\n * than an unexpected abort (non-retryable cancel).\n */\nexport const XHR_BROWSER_TIMEOUT_FLAG = '_lifecycleion_xhr_browser_timeout';\n\nexport const HTTP_METHODS: ReadonlyArray<HTTPMethod> = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n];\n\n/**\n * Exact-match request headers that browsers either forbid outright or do not\n * let this client set reliably via plain Fetch/XHR headers.\n *\n * Prefix-based rules like `proxy-*` and `sec-*` are handled in `header-utils.ts`.\n */\nexport const BROWSER_RESTRICTED_HEADERS: ReadonlySet<string> = new Set([\n // Encoding / CORS negotiation headers controlled by the browser.\n 'accept-charset',\n 'accept-encoding',\n 'access-control-request-headers',\n 'access-control-request-method',\n 'access-control-request-private-network',\n\n // Connection-level transport headers.\n 'connection',\n 'content-length',\n 'date',\n 'expect',\n 'host',\n 'keep-alive',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'via',\n\n // Browser-managed request context / privacy headers.\n 'cookie',\n 'dnt',\n 'origin',\n 'referer',\n 'set-cookie',\n 'user-agent',\n]);\n\nexport const BROWSER_RESTRICTED_HEADER_PREFIXES: ReadonlyArray<string> = [\n 'proxy-',\n 'sec-',\n];\n\n/**\n * Headers that can tunnel the real method through POST. Browsers block these\n * when they try to smuggle forbidden transport methods.\n */\nexport const BROWSER_METHOD_OVERRIDE_HEADER_NAMES: ReadonlySet<string> =\n new Set(['x-http-method', 'x-http-method-override', 'x-method-override']);\n\n/**\n * Methods that browsers do not allow request headers to tunnel via the\n * override headers above.\n */\nexport const BROWSER_FORBIDDEN_METHOD_OVERRIDE_VALUES: ReadonlySet<string> =\n new Set(['connect', 'trace', 'track']);\n\nexport const DEFAULT_MAX_REDIRECTS = 5;\n\n/**\n * Redirect responses that carry a follow-up `Location` hop. `300` and `304`\n * are excluded because they do not represent an automatic redirect here.\n */\nexport const REDIRECT_STATUS_CODES: ReadonlySet<number> = new Set([\n // 301 Moved Permanently\n 301,\n // 302 Found\n 302,\n\n // 303 See Other\n 303,\n\n // 307 Temporary Redirect\n 307,\n // 308 Permanent Redirect\n 308,\n]);\n","import qs from 'qs';\nimport {\n matchesWildcardDomain,\n normalizeDomain,\n} from '../domain-utils/domain-utils';\nimport type {\n ContentType,\n RequestPhaseName,\n HTTPClientConfig,\n AdapterType,\n} from './types';\n\n/**\n * If `path` is an absolute HTTP(S) URL, returns its canonical `href` (normalized\n * scheme/host casing). Otherwise `null`. Protocol-relative `//host` is handled\n * separately in `buildURL`.\n *\n * Rules differ from `resolveAbsoluteURL` (which accepts any absolute scheme).\n * One parse here per request is negligible next to network I/O.\n */\nfunction tryAbsoluteWebHref(path: string): string | null {\n if (!path || path.startsWith('//')) {\n return null;\n }\n\n try {\n const u = new URL(path);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n return u.href;\n }\n } catch {\n // not parseable as absolute\n }\n\n return null;\n}\n\n/**\n * Builds a request URL string from `baseURL`, `path`, and optional query params\n * (`qs` — nested objects and arrays supported).\n *\n * **Relative paths (usual case)** — When `baseURL` is set and `path` is not\n * absolute, `path` is joined to `baseURL` (leading slash normalized). Example:\n * `baseURL: https://api.test`, `path: /v1/users` → `https://api.test/v1/users`.\n *\n * **Absolute / protocol-relative `path` (escape hatch)** — If `path` is a full\n * `http:` or `https:` URL, it is **not** prefixed with `baseURL` (after\n * normalization via `URL#href`). The same applies to protocol-relative URLs\n * (`//cdn.example/x`): they are left for {@link resolveAbsoluteURL} to resolve\n * using the client `baseURL`’s scheme. Use this for one-off cross-origin calls,\n * CDN assets, or URLs returned by APIs; for strict per-origin clients, prefer\n * relative paths and a dedicated client or `HTTPClient.createSubClient()` per\n * origin.\n */\nexport function buildURL(\n baseURL: string | undefined,\n path: string,\n params?: Record<string, unknown>,\n): string {\n let url: string;\n\n const absoluteHref = tryAbsoluteWebHref(path);\n\n if (baseURL && absoluteHref === null && !path.startsWith('//')) {\n // Avoid double slashes when joining base + path\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const p = path.startsWith('/') ? path : `/${path}`;\n url = `${base}${p}`;\n } else if (absoluteHref !== null) {\n url = absoluteHref;\n } else {\n url = path;\n }\n\n if (params && Object.keys(params).length > 0) {\n const [urlWithoutHash, hash = ''] = url.split('#', 2);\n const queryStartIndex = urlWithoutHash.indexOf('?');\n\n if (queryStartIndex === -1) {\n const queryString = qs.stringify(params, { addQueryPrefix: true });\n url = `${urlWithoutHash}${queryString}${hash ? `#${hash}` : ''}`;\n } else {\n const basePath = urlWithoutHash.slice(0, queryStartIndex);\n const existingQuery = urlWithoutHash.slice(queryStartIndex + 1);\n // Fragments are preserved only as part of the caller's URL string.\n // They are not transmitted in HTTP requests, but keeping them intact\n // makes buildURL safer as a general-purpose URL composition helper.\n const mergedParams = {\n ...qs.parse(existingQuery),\n ...params,\n };\n\n const queryString = qs.stringify(mergedParams, { addQueryPrefix: true });\n url = `${basePath}${queryString}${hash ? `#${hash}` : ''}`;\n }\n }\n\n return url;\n}\n\n/**\n * Best-effort absolute URL for logging, redirects, and hop metadata.\n *\n * - If `url` parses as an absolute URL (has a scheme), returns normalized `href`.\n * - Otherwise, when `baseURL` is set, resolves `url` against it (path-relative,\n * same-host relative, protocol-relative `//host`, query-only, etc.).\n * - If neither works, returns `url` unchanged (callers without `baseURL` may still\n * see path-only strings).\n */\nexport function resolveAbsoluteURL(url: string, baseURL?: string): string {\n if (!url) {\n return url;\n }\n\n try {\n return new URL(url).href;\n } catch {\n // Not a standalone absolute URL\n }\n\n if (baseURL) {\n try {\n const base = baseURL.endsWith('/') ? baseURL : `${baseURL}/`;\n return new URL(url, base).href;\n } catch {\n // fall through\n }\n }\n\n return url;\n}\n\n/**\n * Browser-aware absolute URL resolution used by HTTPClient before interceptors\n * and adapter dispatch. Starts with normal baseURL resolution, then falls back\n * to the current page/worker location when running in a browser-like runtime.\n */\nexport function resolveAbsoluteURLForRuntime(\n url: string,\n baseURL: string | undefined,\n isBrowserRuntime: boolean,\n): string {\n const resolved = resolveAbsoluteURL(url, baseURL);\n\n if (\n !isBrowserRuntime ||\n resolved.startsWith('http://') ||\n resolved.startsWith('https://')\n ) {\n return resolved;\n }\n\n const browserBase = getBrowserResolutionBase();\n\n if (!browserBase) {\n return resolved;\n }\n\n return resolveAbsoluteURL(resolved, browserBase);\n}\n\nfunction getBrowserResolutionBase(): string | undefined {\n if (\n typeof document !== 'undefined' &&\n typeof document.baseURI === 'string' &&\n document.baseURI\n ) {\n return document.baseURI;\n }\n\n if (\n typeof window !== 'undefined' &&\n window.location &&\n typeof window.location.href === 'string' &&\n window.location.href\n ) {\n return window.location.href;\n }\n\n const globalLocation = (globalThis as { location?: { href?: unknown } })\n .location;\n\n if (\n globalLocation &&\n typeof globalLocation.href === 'string' &&\n globalLocation.href\n ) {\n return globalLocation.href;\n }\n\n const selfLocation = (\n globalThis as { self?: { location?: { href?: unknown } } }\n ).self?.location;\n\n if (\n selfLocation &&\n typeof selfLocation.href === 'string' &&\n selfLocation.href\n ) {\n return selfLocation.href;\n }\n\n return undefined;\n}\n\n/**\n * Normalizes header keys to lowercase.\n */\nexport function normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = value;\n }\n\n return result;\n}\n\n/**\n * Merges multiple request-header objects, normalizing keys to lowercase.\n * Later objects win on conflict. Array values replace earlier scalars/arrays\n * wholesale, and single-item arrays are collapsed back to a plain string.\n */\nexport function mergeHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? normalizeMergedHeaderArray(value)\n : String(value);\n }\n }\n\n return result;\n}\n\nfunction normalizeMergedHeaderArray(value: string[]): string | string[] {\n const normalized = value.map((item) => String(item));\n return normalized.length === 1 ? normalized[0] : normalized;\n}\n\nexport function mergeObservedHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? value.map((item) => String(item))\n : String(value);\n }\n }\n\n return result;\n}\n\n/**\n * Parses the Content-Type header into a ContentType enum value.\n */\nexport function parseContentType(\n contentTypeHeader: string | undefined,\n): ContentType {\n if (!contentTypeHeader) {\n return 'binary';\n } else {\n const lower = contentTypeHeader.trim().toLowerCase();\n\n if (lower.includes('application/json') || lower.includes('+json')) {\n return 'json';\n } else if (lower.startsWith('text/')) {\n return 'text';\n } else if (lower.includes('application/x-www-form-urlencoded')) {\n return 'text';\n } else {\n return 'binary';\n }\n }\n}\n\n/**\n * Validates adapter/runtime combinations and redirect config before a client\n * is constructed, so unsupported browser-only/server-only options fail fast\n * with clear errors instead of surfacing later during request dispatch.\n */\nexport function assertSupportedAdapterRuntimeAndConfig(\n config: HTTPClientConfig,\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): void {\n if (\n config.baseURL !== undefined &&\n requiresAbsoluteBaseURL(adapterType, isBrowserRuntime)\n ) {\n assertValidBaseURL(config.baseURL);\n }\n\n if (config.maxRedirects !== undefined && config.followRedirects !== true) {\n throw new Error('HTTPClient maxRedirects requires followRedirects: true.');\n }\n\n if (\n config.followRedirects === true &&\n config.maxRedirects !== undefined &&\n config.maxRedirects < 1\n ) {\n throw new Error(\n 'HTTPClient maxRedirects must be greater than or equal to 1 when followRedirects is true.',\n );\n }\n\n if (adapterType === 'xhr' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with XHR adapter. Set followRedirects: false or use a different adapter/runtime.',\n );\n }\n\n if (adapterType === 'xhr' && !hasXMLHttpRequestGlobal()) {\n throw new Error(\n 'HTTPClient XHR adapter is not supported when XMLHttpRequest is unavailable. Use a browser runtime, install a test shim, or switch to the FetchAdapter/NodeAdapter.',\n );\n }\n\n if (!isBrowserRuntime) {\n return;\n }\n\n if (adapterType === 'node') {\n throw new Error(\n 'HTTPClient Node adapter is not supported in browser environments.',\n );\n }\n\n // MockAdapter is intentionally allowed in browser runtimes: it is an\n // in-memory test adapter, so cookie jars and redirect following are local\n // simulation features rather than forbidden browser networking controls.\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.cookieJar) {\n throw new Error(\n `HTTPClient cookieJar is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers manage cookies automatically.`,\n );\n }\n\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.userAgent) {\n throw new Error(\n `HTTPClient userAgent is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers do not allow overriding the User-Agent header.`,\n );\n }\n\n if (adapterType === 'fetch' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with FetchAdapter in browser environments. Set followRedirects: false or use a server runtime.',\n );\n }\n}\n\nfunction requiresAbsoluteBaseURL(\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): boolean {\n if (adapterType === 'node' || adapterType === 'mock') {\n return true;\n }\n\n if (adapterType === 'fetch' && !isBrowserRuntime) {\n return true;\n }\n\n return false;\n}\n\nfunction hasXMLHttpRequestGlobal(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof (globalThis as { XMLHttpRequest?: unknown }).XMLHttpRequest ===\n 'function'\n );\n}\n\n/**\n * Converts a Headers object (from fetch) into AdapterResponse headers.\n * `set-cookie` is extracted as `string[]` via `getSetCookie()` — the Fetch API\n * would otherwise incorrectly comma-join multiple Set-Cookie values.\n * All other headers are extracted as plain strings.\n */\nexport function extractFetchHeaders(\n headers: Headers,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of headers.entries()) {\n const lower = key.toLowerCase();\n\n if (lower !== 'set-cookie') {\n result[lower] = value;\n }\n }\n\n // Use getSetCookie() when available (Bun, Node 18.14+, modern browsers)\n if (typeof headers.getSetCookie === 'function') {\n const setCookies = headers.getSetCookie();\n\n if (setCookies.length > 0) {\n result['set-cookie'] = setCookies;\n }\n } else {\n // Fallback: headers.get() comma-joins — split on ', ' is unreliable for\n // cookies but better than nothing on older runtimes\n const raw = headers.get('set-cookie');\n\n if (raw) {\n result['set-cookie'] = [raw];\n }\n }\n\n return result;\n}\n\n/**\n * Lowercases all keys on adapter/response header objects. `HTTPClient` runs\n * this on each adapter response before {@link CookieJar.processResponseHeaders}.\n * The jar also normalizes so the same shapes work when feeding headers directly.\n *\n * - Non–`set-cookie` values: if an array appears (unexpected), the first\n * element is kept when read via {@link scalarHeader}.\n * - `set-cookie`: stored as `string[]` — each array entry is one full\n * `Set-Cookie` header line (one cookie). A single string value becomes a\n * one-element array. If the same header appears under keys that differ only\n * by case, those lines are appended in the order they appear on the input\n * object.\n */\nexport function normalizeAdapterResponseHeaders(\n headers: Record<string, string | string[]>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n\n if (lower === 'set-cookie') {\n const chunk = Array.isArray(value) ? value : [value];\n const existing = result[lower];\n\n if (existing === undefined) {\n result[lower] = chunk;\n } else {\n const existingLines = Array.isArray(existing) ? existing : [existing];\n result[lower] = [...existingLines, ...chunk];\n }\n } else {\n result[lower] = Array.isArray(value) ? (value[0] ?? '') : value;\n }\n }\n\n return result;\n}\n\n/**\n * Reads a single-valued header when keys are already lowercase (e.g. after\n * {@link mergeHeaders} on requests or {@link normalizeAdapterResponseHeaders}\n * on responses). If the stored value is `string[]`, returns the first entry.\n */\nexport function scalarHeader(\n headers: Record<string, string | string[]>,\n lowercaseName: string,\n): string | undefined {\n const v = headers[lowercaseName];\n\n if (v === undefined) {\n return undefined;\n }\n\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Resolves a redirect target from response headers when the adapter can\n * observe a redirect response but the client may not follow it itself.\n */\nexport function resolveDetectedRedirectURL(\n requestURL: string,\n status: number,\n headers: Record<string, string | string[]>,\n baseURL?: string,\n): string | undefined {\n if (![301, 302, 303, 307, 308].includes(status)) {\n return undefined;\n }\n\n const location = scalarHeader(headers, 'location');\n\n if (!location) {\n return undefined;\n }\n\n try {\n const absoluteRequestURL = resolveAbsoluteURL(requestURL, baseURL);\n return new URL(location, absoluteRequestURL).toString();\n } catch {\n return location;\n }\n}\n\nexport function assertValidBaseURL(\n baseURL: string,\n fieldName = 'baseURL',\n): void {\n try {\n const url = new URL(baseURL);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('unsupported protocol');\n }\n } catch {\n throw new Error(\n `HTTPClient ${fieldName} must be an absolute http(s) URL (for example \"https://api.example.com\").`,\n );\n }\n}\n\n/**\n * Detects whether the current runtime looks like a browser environment.\n */\nexport function isBrowserEnvironment(): boolean {\n if (typeof globalThis === 'undefined') {\n return false;\n }\n\n if ('window' in globalThis && 'document' in globalThis) {\n return true;\n }\n\n const workerGlobalScope = (\n globalThis as {\n WorkerGlobalScope?: abstract new (...args: never[]) => unknown;\n }\n ).WorkerGlobalScope;\n\n if (\n typeof workerGlobalScope === 'function' &&\n (globalThis as { self?: unknown }).self instanceof workerGlobalScope\n ) {\n return true;\n }\n\n const constructorName = globalThis.constructor?.name;\n\n return (\n !('window' in globalThis) &&\n !('document' in globalThis) &&\n typeof constructorName === 'string' &&\n constructorName.endsWith('WorkerGlobalScope')\n );\n}\n\n/**\n * Serializes the request body and returns the body + inferred content-type.\n * If `formData` is provided, it takes precedence over `body`.\n */\nexport function serializeBody(body: unknown): {\n body: string | Uint8Array | FormData | null;\n contentType: string | null;\n} {\n assertSupportedRequestBody(body);\n\n if (body instanceof FormData) {\n return { body, contentType: null }; // browser/runtime sets multipart boundary automatically\n } else if (body === undefined || body === null) {\n return { body: null, contentType: null };\n } else if (typeof body === 'string') {\n return { body, contentType: 'text/plain; charset=utf-8' };\n } else if (body instanceof Uint8Array) {\n return { body, contentType: 'application/octet-stream' };\n } else if (Array.isArray(body) || isPlainJSONBodyObject(body)) {\n return {\n body: JSON.stringify(body),\n contentType: 'application/json; charset=utf-8',\n };\n } else {\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n }\n}\n\nexport function assertSupportedRequestBody(body: unknown): void {\n if (\n body === undefined ||\n body === null ||\n typeof body === 'string' ||\n body instanceof Uint8Array ||\n body instanceof FormData ||\n Array.isArray(body) ||\n isPlainJSONBodyObject(body)\n ) {\n return;\n }\n\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n}\n\nexport function isPlainJSONBodyObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n\n const prototype = Reflect.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/**\n * Extracts the hostname from a URL string. Returns empty string on failure.\n */\nexport function extractHostname(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/**\n * Wildcard hostname matching backed by {@link matchesWildcardDomain}.\n *\n * - `*` — global wildcard, matches any valid hostname including apex domains\n * - `*.example.com` — matches exactly one subdomain label (`api.example.com`) but not deeper levels or the apex\n * - `**.example.com` — matches one or more subdomain labels (`api.example.com`, `a.b.example.com`) but not the apex\n * - Exact patterns (no `*`) — normalized comparison, case-insensitive\n * - PSL tail guard active: `*.com`, `**.co.uk`, etc. never match\n * - Pseudo-TLD suffix wildcards are rejected just like PSL tails (`*.localhost`, `*.local`, etc.)\n */\nexport function matchesHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.includes('*')) {\n return matchesWildcardDomain(hostname, pattern);\n }\n\n const normalizedHostname = normalizeDomain(hostname);\n const normalizedPattern = normalizeDomain(pattern);\n return normalizedHostname !== '' && normalizedHostname === normalizedPattern;\n}\n\n/**\n * Checks whether a dot-path key exists in a nested object.\n * Arrays are not traversed — only plain objects at each segment.\n */\nfunction hasNestedKey(obj: Record<string, unknown>, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n return false;\n }\n\n if (!(part in (current as Record<string, unknown>))) {\n return false;\n }\n\n current = (current as Record<string, unknown>)[part];\n }\n\n return true;\n}\n\nfunction normalizeMimeType(value: string): string {\n return value.split(';', 1)[0].trim().toLowerCase();\n}\n\nfunction matchesContentTypePattern(\n actualHeader: string,\n pattern: string,\n): boolean {\n const actual = normalizeMimeType(actualHeader);\n const expected = normalizeMimeType(pattern);\n\n if (!actual || !expected) {\n return false;\n }\n\n if (expected.endsWith('/*')) {\n const expectedType = expected.slice(0, -2);\n const slashIndex = actual.indexOf('/');\n\n if (slashIndex === -1) {\n return false;\n }\n\n return actual.slice(0, slashIndex) === expectedType;\n }\n\n return actual === expected;\n}\n\n/**\n * Tests whether a request context matches an interceptor/observer filter.\n *\n * Each filter field is optional — omitting it skips that check entirely.\n * All specified fields must match for the function to return true.\n * Within each field, values are matched with OR logic (any one match is sufficient).\n *\n * - `phases`: **OR** allowlist on `phaseType` ({@link RequestPhaseName}). Skipped when\n * `filter.phases` is omitted or empty.\n * - `statusCodes`: skipped if `context.status` is absent.\n * - `methods`: skipped if `context.method` is absent.\n * - `hosts`: supports exact hostnames and wildcard patterns. `*.example.com` matches\n * exactly one subdomain label; `**.example.com` matches any depth. Neither matches the\n * apex — list it explicitly. PSL tail guard prevents `*.com`-style patterns. `*` is a\n * global wildcard that matches any valid hostname. Skipped if `context.requestURL` is absent.\n * - `schemes`: `'http'` or `'https'`. `requestURL` is absolute whenever the\n * request could be resolved before dispatch. For `MockAdapter`, path-only\n * requests without a client `baseURL` are materialized as `http://localhost/...`;\n * browser adapters fall back to `window.location`, and the Node adapter requires\n * absolute URLs. Skipped only when `requestURL` is absent.\n * - `bodyContainsKeys`: supports dot paths (e.g. `data.results`). Each segment in\n * the path must resolve to a plain object for traversal to continue — the final\n * value can be anything (array, string, null, etc). Array indexing is not supported.\n * Skipped when `kind` is `'error'`.\n */\nexport function matchesFilter(\n filter: {\n statusCodes?: number[];\n methods?: string[];\n bodyContainsKeys?: string[];\n hosts?: string[];\n schemes?: ('http' | 'https')[];\n phases?: RequestPhaseName[];\n contentTypes?: ContentType[];\n contentTypeHeaders?: string[];\n },\n context: {\n status?: number;\n method?: string;\n body?: unknown;\n requestURL?: string;\n contentType?: ContentType;\n contentTypeHeader?: string;\n },\n phaseType: RequestPhaseName,\n kind: 'request' | 'response' | 'error',\n): boolean {\n if (\n filter.phases &&\n filter.phases.length > 0 &&\n !filter.phases.includes(phaseType)\n ) {\n return false;\n }\n\n if (filter.statusCodes && context.status !== undefined) {\n if (!filter.statusCodes.includes(context.status)) {\n return false;\n }\n }\n\n if (filter.methods && context.method) {\n if (!filter.methods.includes(context.method)) {\n return false;\n }\n }\n\n if (filter.contentTypes && filter.contentTypes.length > 0) {\n if (\n !context.contentType ||\n !filter.contentTypes.includes(context.contentType)\n ) {\n return false;\n }\n }\n\n if (filter.contentTypeHeaders && filter.contentTypeHeaders.length > 0) {\n if (\n !context.contentTypeHeader ||\n !filter.contentTypeHeaders.some((pattern) =>\n matchesContentTypePattern(context.contentTypeHeader as string, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (\n kind !== 'error' &&\n filter.bodyContainsKeys &&\n filter.bodyContainsKeys.length > 0\n ) {\n if (\n !context.body ||\n typeof context.body !== 'object' ||\n Array.isArray(context.body)\n ) {\n return false;\n }\n\n const body = context.body as Record<string, unknown>;\n\n if (!filter.bodyContainsKeys.some((k) => hasNestedKey(body, k))) {\n return false;\n }\n }\n\n if (filter.hosts && context.requestURL) {\n const hostname = extractHostname(context.requestURL);\n\n if (\n !filter.hosts.some((pattern: string) =>\n matchesHostPattern(hostname, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (filter.schemes && filter.schemes.length > 0 && context.requestURL) {\n let scheme: 'http' | 'https' | null = null;\n\n try {\n const parsedScheme = new URL(context.requestURL).protocol.replace(\n ':',\n '',\n );\n\n if (parsedScheme === 'http' || parsedScheme === 'https') {\n scheme = parsedScheme;\n }\n } catch {\n scheme = null;\n }\n\n if (!scheme || !filter.schemes.includes(scheme)) {\n return false;\n }\n }\n\n return true;\n}\n","import { XHR_BROWSER_TIMEOUT_FLAG } from '../consts';\nimport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n} from '../types';\nimport { resolveAbsoluteURLForRuntime } from '../utils';\n\n/**\n * XHR-based adapter for environments that expose `XMLHttpRequest`. Primary\n * advantage over FetchAdapter is real per-chunk upload and download progress\n * via `xhr.upload.onprogress` / `xhr.onprogress`. FetchAdapter only fires 0%\n * and 100% because the Fetch API has no streaming upload and requires\n * buffering the full response to read body bytes.\n *\n * XHR constraints compared to FetchAdapter and NodeAdapter:\n * - `followRedirects: false` is required — XHR offers no opt-out from\n * automatic redirect following, so individual hops cannot be observed or\n * controlled. Redirect following is unsupported and treated as an error.\n * - In browser runtimes, cookies, CORS, and restricted headers (e.g. Cookie,\n * User-Agent) are browser-managed; `cookieJar` must not be passed there.\n */\nexport class XHRAdapter implements HTTPAdapter {\n public getType(): AdapterType {\n return 'xhr';\n }\n\n public send(request: AdapterRequest): Promise<AdapterResponse> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n // responseType 'arraybuffer' gives us a raw ArrayBuffer on load,\n // consistent with how FetchAdapter and NodeAdapter deliver body bytes.\n xhr.open(request.method, request.requestURL);\n xhr.responseType = 'arraybuffer';\n\n // Timeout is managed by the client via the abort signal — the client's\n // per-attempt timer fires AbortController.abort(), which propagates to\n // xhr.abort() through the signal listener below. We disable XHR's own\n // timeout mechanism (0 = no timeout) so the client retains full control.\n // The 'timeout' event listener below is kept as a defensive fallback in\n // case a browser fires it anyway (e.g. a hard-coded internal limit).\n xhr.timeout = 0;\n\n // --- Request headers ---\n //\n // Calling setRequestHeader multiple times for the same key causes XHR to\n // combine values with \", \" per spec — which is correct for all headers\n // the browser allows scripts to set. Cookie is a forbidden header name\n // and is silently dropped by the browser regardless; the browser manages\n // cookies on its own.\n for (const [key, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) {\n for (const v of value) {\n xhr.setRequestHeader(key, v);\n }\n } else {\n xhr.setRequestHeader(key, value);\n }\n }\n\n // --- Abort signal ---\n //\n // Check for pre-aborted signal before calling xhr.send — if we called\n // send first and then aborted, the abort event fires asynchronously and\n // we'd resolve the promise rather than reject it with an AbortError.\n if (request.signal) {\n if (request.signal.aborted) {\n reject(new DOMException('Request aborted', 'AbortError'));\n return;\n }\n\n request.signal.addEventListener(\n 'abort',\n () => {\n xhr.abort();\n },\n // once: true — the XHR is already done after the first abort, no\n // need to keep the listener alive and risk a second call.\n { once: true },\n );\n }\n\n // --- Upload progress ---\n\n // Fire initial 0% upload progress before any bytes leave the browser,\n // mirroring the FetchAdapter pattern so callers see a consistent first\n // event regardless of adapter.\n request.onUploadProgress?.({ loaded: 0, total: 0, progress: 0 });\n\n // Real per-chunk upload progress — the main advantage over FetchAdapter,\n // which has no streaming upload and can only fire 0% then 100%.\n // Deduplication guard — upload.progress and upload.load can both report\n // 100% (see upload.load listener below for details).\n let didFireUpload100 = false;\n let uploadedBytes = 0;\n let uploadTotalBytes = 0;\n\n xhr.upload.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireUpload100 = true;\n }\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(uploadTotalBytes, event.total);\n\n request.onUploadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // 100% upload fires as soon as all bytes are sent, always before xhr.load\n // per spec. Skip if upload.progress already reported 100% to avoid a\n // duplicate event. We still track whether this fired so xhr.load can use\n // it as a fallback for environments that skip upload.load entirely.\n let didUploadComplete = false;\n\n xhr.upload.addEventListener('load', (event) => {\n didUploadComplete = true;\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(\n uploadTotalBytes,\n event.total || event.loaded,\n );\n\n if (!didFireUpload100) {\n const finalLoaded = uploadedBytes > 0 ? uploadedBytes : 1;\n const finalTotal = uploadTotalBytes > 0 ? uploadTotalBytes : 1;\n request.onUploadProgress?.({\n loaded: finalLoaded,\n total: finalTotal,\n progress: 1,\n });\n }\n });\n\n // --- Download progress ---\n\n // Real per-chunk download progress. Same advantage over FetchAdapter:\n // FetchAdapter buffers the full response body before firing any progress,\n // so it can only ever report 0% then 100%.\n // Deduplication guard — when Content-Length is known and the final\n // progress chunk reaches 100%, xhr.load would otherwise fire it again.\n let didFireDownload100 = false;\n let downloadedBytes = 0;\n let downloadTotalBytes = 0;\n\n xhr.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireDownload100 = true;\n }\n\n downloadedBytes = Math.max(downloadedBytes, event.loaded);\n downloadTotalBytes = Math.max(downloadTotalBytes, event.total);\n\n request.onDownloadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // --- Load (success) ---\n\n xhr.addEventListener('load', () => {\n // Detect browser-followed redirects.\n //\n // In a browser, FetchAdapter uses `redirect: 'manual'` which yields an\n // opaqueredirect response (status 0) — redirects are intercepted before\n // they happen. XHR has no equivalent opt-out; the browser always follows\n // redirects automatically. We detect them after-the-fact by comparing\n // xhr.responseURL (the final URL after all hops) to the original URL.\n //\n // Both browser adapters surface the same signal: status 0 +\n // wasRedirectDetected, which routes through HTTPClient's\n // redirect_disabled error path so callers get a consistent isFailed\n // response regardless of adapter.\n if (\n xhr.responseURL &&\n didBrowserFollowRedirect(xhr.responseURL, request.requestURL)\n ) {\n // The browser completed the transport and surfaced the final URL even\n // though the client will treat the result as redirect_disabled, so\n // emit terminal progress before returning the synthetic redirect\n // response.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: downloadedBytes,\n total: downloadTotalBytes,\n progress: 1,\n });\n }\n\n resolve({\n status: 0,\n wasRedirectDetected: true,\n // XHR exposes the post-redirect final URL via responseURL. Browser\n // fetch opaque redirects do not, so this is intentionally\n // adapter-specific and surfaced separately from requestURL.\n detectedRedirectURL: xhr.responseURL,\n headers: {},\n body: null,\n });\n return;\n }\n\n // Fallback: upload.load didn't fire (no request body, or the browser\n // skipped the event). Ensure callers always see a 100% upload event,\n // unless upload.progress already reported it.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n const body = readResponseBody(request.method, xhr);\n\n // Final 100% download progress — skip if a progress event already\n // fired exactly 100% (Content-Length known and final chunk completed it).\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: body?.length ?? 0,\n total: body?.length ?? 0,\n progress: 1,\n });\n }\n\n resolve({\n status: xhr.status,\n headers: parseXHRResponseHeaders(xhr.getAllResponseHeaders()),\n body,\n });\n });\n\n // --- Error / timeout / abort ---\n\n // The error event fires for network-level failures (DNS failure, refused\n // connection, CORS rejection). It never fires for HTTP error status codes\n // (4xx, 5xx) — those arrive on the load event with a real status.\n xhr.addEventListener('error', () => {\n resolve({\n status: 0,\n isTransportError: true,\n headers: {},\n body: null,\n errorCause: new Error('XHR network error'),\n });\n });\n\n // Defensive fallback: fires if the browser has a hard-coded internal\n // timeout limit (xhr.timeout is 0 so we never set one ourselves). Mark\n // the error so HTTPClient classifies it as a timeout (retryable) rather\n // than an unexpected abort (non-retryable cancel).\n xhr.addEventListener('timeout', () => {\n reject(\n Object.assign(new DOMException('Request timed out', 'AbortError'), {\n [XHR_BROWSER_TIMEOUT_FLAG]: true,\n }),\n );\n });\n\n xhr.addEventListener('abort', () => {\n reject(new DOMException('Request aborted', 'AbortError'));\n });\n\n xhr.send(prepareBody(request.body));\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the response body as a Uint8Array, or null for response types that\n * carry no body (HEAD, 204 No Content, 304 Not Modified).\n */\nfunction readResponseBody(\n method: string,\n xhr: XMLHttpRequest,\n): Uint8Array | null {\n if (method === 'HEAD' || xhr.status === 204 || xhr.status === 304) {\n return null;\n }\n\n if (xhr.response instanceof ArrayBuffer) {\n return new Uint8Array(xhr.response);\n }\n\n return null;\n}\n\n/**\n * Converts the adapter request body to a value accepted by `xhr.send()`.\n * `string`, `Uint8Array` (BufferSource), and `FormData` are all valid\n * `XMLHttpRequestBodyInit` values — the cast is safe for the body types\n * the client produces.\n */\nfunction prepareBody(\n body: AdapterRequest['body'],\n): XMLHttpRequestBodyInit | null {\n if (body === null) {\n return null;\n }\n\n return body as XMLHttpRequestBodyInit;\n}\n\n/**\n * Parses the raw header string from `xhr.getAllResponseHeaders()` into a\n * lowercase-keyed record.\n *\n * `getAllResponseHeaders()` returns CRLF-delimited `name: value` lines. When\n * a server sends multiple headers with the same name the browser combines them\n * into a single comma-joined line for most headers, but emits each `Set-Cookie`\n * value as its own line (per spec) to avoid ambiguity with the comma in cookie\n * values. Those are collected here as `string[]` to match the\n * `AdapterResponse.headers` contract.\n *\n * Note: browsers unconditionally block `Set-Cookie` and `Set-Cookie2` from\n * `getAllResponseHeaders()` per the XHR spec, so the `set-cookie` array\n * branch below is effectively unreachable in a real browser — it exists to\n * satisfy the shared `AdapterResponse` type contract.\n */\nfunction parseXHRResponseHeaders(\n raw: string,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n if (!raw) {\n return result;\n }\n\n for (const line of raw.split('\\r\\n')) {\n // Lines are `Name: value` pairs separated by the first colon.\n // indexOf is used (not split) so colons in the value are preserved.\n const colonIndex = line.indexOf(':');\n\n if (colonIndex < 0) {\n // No colon — malformed or trailing empty line; skip\n continue;\n }\n\n // Lowercase to normalize across servers (header names are case-insensitive)\n const key = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n\n if (!key) {\n // Colon at position 0 — no name; skip\n continue;\n }\n\n if (key === 'set-cookie') {\n // Each Set-Cookie directive arrives as its own line — collect into an\n // array so callers never need to split on commas (which are valid inside\n // cookie values). Guarded by the XHR spec in standard browsers, but kept\n // for correctness in legacy environments or platforms with non-standard\n // XHR implementations.\n const existing = result['set-cookie'];\n\n if (existing === undefined) {\n result['set-cookie'] = [value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result['set-cookie'] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Compares URLs as browsers evaluate request destinations:\n * - strips hash fragments (not sent over HTTP)\n * - relies on URL normalization for equivalent forms\n * (default ports, dot segments, encoding normalization, etc.)\n */\nfunction didBrowserFollowRedirect(\n responseURL: string,\n requestURL: string,\n): boolean {\n try {\n const normalizedResponse = new URL(responseURL);\n normalizedResponse.hash = '';\n\n const normalizedRequest = new URL(\n resolveAbsoluteURLForRuntime(requestURL, undefined, true),\n normalizedResponse.href,\n );\n normalizedRequest.hash = '';\n\n return normalizedResponse.href !== normalizedRequest.href;\n } catch {\n // Fallback for non-URL inputs: preserve prior behavior.\n return responseURL !== requestURL;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/http-client-xhr/index.ts","../../../src/lib/http-client/consts.ts","../../../src/lib/http-client/utils.ts","../../../src/lib/http-client/adapters/xhr-adapter.ts"],"sourcesContent":["export { XHRAdapter } from '../http-client/adapters/xhr-adapter';\n\n/**\n * Adapter contract types, re-exported so code driving an adapter directly —\n * building AdapterRequest objects and calling send() rather than going through\n * HTTPClient — does not have to import types from 'lifecycleion/http-client',\n * the module it is specifically not using. Type-only, so nothing is added to\n * the runtime bundle.\n */\nexport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n AdapterProgressEvent,\n HTTPMethod,\n} from '../http-client/types';\n","import type { HTTPMethod } from './types';\n\n/**\n * HTTP responses that are plausibly transient and worth retrying when a retry\n * policy is explicitly enabled.\n *\n * `status === 0` is included on purpose because browser/XHR-style adapters can\n * surface \"no real HTTP response\" that way when the network is unavailable or\n * the request otherwise fails before a normal status code is received.\n */\nexport const RETRYABLE_STATUS_CODES: ReadonlySet<number> = new Set([\n // 0: Browser/XHR-style \"no response\" status.\n 0,\n\n // 408 Request Timeout\n 408,\n // 429 Too Many Requests\n 429,\n\n // 500 Internal Server Error\n 500,\n // 502 Bad Gateway\n 502,\n // 503 Service Unavailable\n 503,\n // 504 Gateway Timeout\n 504,\n\n // 507 Insufficient Storage\n 507,\n // 509 Bandwidth Limit Exceeded (non-standard)\n 509,\n // 520 Unknown Error (Cloudflare)\n 520,\n // 521 Web Server Is Down (Cloudflare)\n 521,\n // 522 Connection Timed Out (Cloudflare)\n 522,\n // 523 Origin Is Unreachable (Cloudflare)\n 523,\n // 524 A Timeout Occurred (Cloudflare)\n 524,\n // 598 Network Read Timeout Error (non-standard)\n 598,\n // 599 Network Connect Timeout Error (non-standard)\n 599,\n]);\n\n/**\n * Methods RFC 9110 does not define as idempotent, so replaying one may apply\n * the same change twice.\n *\n * `PUT` and `DELETE` are absent on purpose: both are idempotent by definition,\n * even though they mutate. Repeating them lands the resource in the same state\n * as doing it once, which is exactly what makes a replay safe.\n */\nexport const NON_IDEMPOTENT_METHODS: ReadonlySet<HTTPMethod> =\n new Set<HTTPMethod>(['POST', 'PATCH']);\n\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-local-client-request-id';\n\nexport const DEFAULT_REQUEST_ATTEMPT_HEADER = 'x-local-client-request-attempt';\n\nexport const DEFAULT_USER_AGENT = 'lifecycleion-http-client';\n\nexport const NON_RETRYABLE_HTTP_CLIENT_CALLBACK_ERROR_FLAG =\n '_lifecycleion_non_retryable_http_client_callback_error';\n\nexport const STREAM_FACTORY_ERROR_FLAG = '_lifecycleion_stream_factory_error';\n\n/**\n * Attached to the AbortError thrown when a StreamResponseFactory returns null\n * or `{ cancel: true, reason? }`. The value is the reason string if provided,\n * or `true` if the factory cancelled without a reason. Lets HTTPClient surface\n * the reason on HTTPClientError.cancelReason.\n */\nexport const STREAM_FACTORY_CANCEL_KEY =\n '_lifecycleion_stream_factory_cancel_reason';\n\nexport const RESPONSE_STREAM_ABORT_FLAG = '_lifecycleion_response_stream_abort';\n\n/**\n * Set on the AbortError thrown by XHRAdapter's defensive `timeout` event\n * listener. Lets HTTPClient classify the error as a timeout (retryable) rather\n * than an unexpected abort (non-retryable cancel).\n */\nexport const XHR_BROWSER_TIMEOUT_FLAG = '_lifecycleion_xhr_browser_timeout';\n\nexport const HTTP_METHODS: ReadonlyArray<HTTPMethod> = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n];\n\n/**\n * Exact-match request headers that browsers either forbid outright or do not\n * let this client set reliably via plain Fetch/XHR headers.\n *\n * Prefix-based rules like `proxy-*` and `sec-*` are handled in `header-utils.ts`.\n */\nexport const BROWSER_RESTRICTED_HEADERS: ReadonlySet<string> = new Set([\n // Encoding / CORS negotiation headers controlled by the browser.\n 'accept-charset',\n 'accept-encoding',\n 'access-control-request-headers',\n 'access-control-request-method',\n 'access-control-request-private-network',\n\n // Connection-level transport headers.\n 'connection',\n 'content-length',\n 'date',\n 'expect',\n 'host',\n 'keep-alive',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'via',\n\n // Browser-managed request context / privacy headers.\n 'cookie',\n 'dnt',\n 'origin',\n 'referer',\n 'set-cookie',\n 'user-agent',\n]);\n\nexport const BROWSER_RESTRICTED_HEADER_PREFIXES: ReadonlyArray<string> = [\n 'proxy-',\n 'sec-',\n];\n\n/**\n * Headers that can tunnel the real method through POST. Browsers block these\n * when they try to smuggle forbidden transport methods.\n */\nexport const BROWSER_METHOD_OVERRIDE_HEADER_NAMES: ReadonlySet<string> =\n new Set(['x-http-method', 'x-http-method-override', 'x-method-override']);\n\n/**\n * Methods that browsers do not allow request headers to tunnel via the\n * override headers above.\n */\nexport const BROWSER_FORBIDDEN_METHOD_OVERRIDE_VALUES: ReadonlySet<string> =\n new Set(['connect', 'trace', 'track']);\n\nexport const DEFAULT_MAX_REDIRECTS = 5;\n\n/**\n * Redirect responses that carry a follow-up `Location` hop. `300` and `304`\n * are excluded because they do not represent an automatic redirect here.\n */\nexport const REDIRECT_STATUS_CODES: ReadonlySet<number> = new Set([\n // 301 Moved Permanently\n 301,\n // 302 Found\n 302,\n\n // 303 See Other\n 303,\n\n // 307 Temporary Redirect\n 307,\n // 308 Permanent Redirect\n 308,\n]);\n","import qs from 'qs';\nimport {\n matchesWildcardDomain,\n normalizeDomain,\n} from '../domain-utils/domain-utils';\nimport type {\n ContentType,\n RequestPhaseName,\n HTTPClientConfig,\n AdapterType,\n} from './types';\n\n/**\n * If `path` is an absolute HTTP(S) URL, returns its canonical `href` (normalized\n * scheme/host casing). Otherwise `null`. Protocol-relative `//host` is handled\n * separately in `buildURL`.\n *\n * Rules differ from `resolveAbsoluteURL` (which accepts any absolute scheme).\n * One parse here per request is negligible next to network I/O.\n */\nfunction tryAbsoluteWebHref(path: string): string | null {\n if (!path || path.startsWith('//')) {\n return null;\n }\n\n try {\n const u = new URL(path);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n return u.href;\n }\n } catch {\n // not parseable as absolute\n }\n\n return null;\n}\n\n/**\n * Builds a request URL string from `baseURL`, `path`, and optional query params\n * (`qs` — nested objects and arrays supported).\n *\n * **Relative paths (usual case)** — When `baseURL` is set and `path` is not\n * absolute, `path` is joined to `baseURL` (leading slash normalized). Example:\n * `baseURL: https://api.test`, `path: /v1/users` → `https://api.test/v1/users`.\n *\n * **Absolute / protocol-relative `path` (escape hatch)** — If `path` is a full\n * `http:` or `https:` URL, it is **not** prefixed with `baseURL` (after\n * normalization via `URL#href`). The same applies to protocol-relative URLs\n * (`//cdn.example/x`): they are left for {@link resolveAbsoluteURL} to resolve\n * using the client `baseURL`’s scheme. Use this for one-off cross-origin calls,\n * CDN assets, or URLs returned by APIs; for strict per-origin clients, prefer\n * relative paths and a dedicated client or `HTTPClient.createSubClient()` per\n * origin.\n */\nexport function buildURL(\n baseURL: string | undefined,\n path: string,\n params?: Record<string, unknown>,\n): string {\n let url: string;\n\n const absoluteHref = tryAbsoluteWebHref(path);\n\n if (baseURL && absoluteHref === null && !path.startsWith('//')) {\n // Avoid double slashes when joining base + path\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const p = path.startsWith('/') ? path : `/${path}`;\n url = `${base}${p}`;\n } else if (absoluteHref !== null) {\n url = absoluteHref;\n } else {\n url = path;\n }\n\n if (params && Object.keys(params).length > 0) {\n const [urlWithoutHash, hash = ''] = url.split('#', 2);\n const queryStartIndex = urlWithoutHash.indexOf('?');\n\n if (queryStartIndex === -1) {\n const queryString = qs.stringify(params, { addQueryPrefix: true });\n url = `${urlWithoutHash}${queryString}${hash ? `#${hash}` : ''}`;\n } else {\n const basePath = urlWithoutHash.slice(0, queryStartIndex);\n const existingQuery = urlWithoutHash.slice(queryStartIndex + 1);\n // Fragments are preserved only as part of the caller's URL string.\n // They are not transmitted in HTTP requests, but keeping them intact\n // makes buildURL safer as a general-purpose URL composition helper.\n const mergedParams = {\n ...qs.parse(existingQuery),\n ...params,\n };\n\n const queryString = qs.stringify(mergedParams, { addQueryPrefix: true });\n url = `${basePath}${queryString}${hash ? `#${hash}` : ''}`;\n }\n }\n\n return url;\n}\n\n/**\n * Best-effort absolute URL for logging, redirects, and hop metadata.\n *\n * - If `url` parses as an absolute URL (has a scheme), returns normalized `href`.\n * - Otherwise, when `baseURL` is set, resolves `url` against it (path-relative,\n * same-host relative, protocol-relative `//host`, query-only, etc.).\n * - If neither works, returns `url` unchanged (callers without `baseURL` may still\n * see path-only strings).\n */\nexport function resolveAbsoluteURL(url: string, baseURL?: string): string {\n if (!url) {\n return url;\n }\n\n try {\n return new URL(url).href;\n } catch {\n // Not a standalone absolute URL\n }\n\n if (baseURL) {\n try {\n const base = baseURL.endsWith('/') ? baseURL : `${baseURL}/`;\n return new URL(url, base).href;\n } catch {\n // fall through\n }\n }\n\n return url;\n}\n\n/**\n * Browser-aware absolute URL resolution used by HTTPClient before interceptors\n * and adapter dispatch. Starts with normal baseURL resolution, then falls back\n * to the current page/worker location when running in a browser-like runtime.\n */\nexport function resolveAbsoluteURLForRuntime(\n url: string,\n baseURL: string | undefined,\n isBrowserRuntime: boolean,\n): string {\n const resolved = resolveAbsoluteURL(url, baseURL);\n\n if (\n !isBrowserRuntime ||\n resolved.startsWith('http://') ||\n resolved.startsWith('https://')\n ) {\n return resolved;\n }\n\n const browserBase = getBrowserResolutionBase();\n\n if (!browserBase) {\n return resolved;\n }\n\n return resolveAbsoluteURL(resolved, browserBase);\n}\n\nfunction getBrowserResolutionBase(): string | undefined {\n if (\n typeof document !== 'undefined' &&\n typeof document.baseURI === 'string' &&\n document.baseURI\n ) {\n return document.baseURI;\n }\n\n if (\n typeof window !== 'undefined' &&\n window.location &&\n typeof window.location.href === 'string' &&\n window.location.href\n ) {\n return window.location.href;\n }\n\n const globalLocation = (globalThis as { location?: { href?: unknown } })\n .location;\n\n if (\n globalLocation &&\n typeof globalLocation.href === 'string' &&\n globalLocation.href\n ) {\n return globalLocation.href;\n }\n\n const selfLocation = (\n globalThis as { self?: { location?: { href?: unknown } } }\n ).self?.location;\n\n if (\n selfLocation &&\n typeof selfLocation.href === 'string' &&\n selfLocation.href\n ) {\n return selfLocation.href;\n }\n\n return undefined;\n}\n\n/**\n * Normalizes header keys to lowercase.\n */\nexport function normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = value;\n }\n\n return result;\n}\n\n/**\n * Merges multiple request-header objects, normalizing keys to lowercase.\n * Later objects win on conflict. Array values replace earlier scalars/arrays\n * wholesale, and single-item arrays are collapsed back to a plain string.\n */\nexport function mergeHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? normalizeMergedHeaderArray(value)\n : String(value);\n }\n }\n\n return result;\n}\n\nfunction normalizeMergedHeaderArray(value: string[]): string | string[] {\n const normalized = value.map((item) => String(item));\n return normalized.length === 1 ? normalized[0] : normalized;\n}\n\nexport function mergeObservedHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? value.map((item) => String(item))\n : String(value);\n }\n }\n\n return result;\n}\n\n/**\n * Parses the Content-Type header into a ContentType enum value.\n */\nexport function parseContentType(\n contentTypeHeader: string | undefined,\n): ContentType {\n if (!contentTypeHeader) {\n return 'binary';\n } else {\n const lower = contentTypeHeader.trim().toLowerCase();\n\n if (lower.includes('application/json') || lower.includes('+json')) {\n return 'json';\n } else if (lower.startsWith('text/')) {\n return 'text';\n } else if (lower.includes('application/x-www-form-urlencoded')) {\n return 'text';\n } else {\n return 'binary';\n }\n }\n}\n\n/**\n * Validates adapter/runtime combinations and redirect config before a client\n * is constructed, so unsupported browser-only/server-only options fail fast\n * with clear errors instead of surfacing later during request dispatch.\n */\nexport function assertSupportedAdapterRuntimeAndConfig(\n config: HTTPClientConfig,\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): void {\n if (\n config.baseURL !== undefined &&\n requiresAbsoluteBaseURL(adapterType, isBrowserRuntime)\n ) {\n assertValidBaseURL(config.baseURL);\n }\n\n if (config.maxRedirects !== undefined && config.followRedirects !== true) {\n throw new Error('HTTPClient maxRedirects requires followRedirects: true.');\n }\n\n if (\n config.followRedirects === true &&\n config.maxRedirects !== undefined &&\n config.maxRedirects < 1\n ) {\n throw new Error(\n 'HTTPClient maxRedirects must be greater than or equal to 1 when followRedirects is true.',\n );\n }\n\n if (adapterType === 'xhr' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with XHR adapter. Set followRedirects: false or use a different adapter/runtime.',\n );\n }\n\n if (adapterType === 'xhr' && !hasXMLHttpRequestGlobal()) {\n throw new Error(\n 'HTTPClient XHR adapter is not supported when XMLHttpRequest is unavailable. Use a browser runtime, install a test shim, or switch to the FetchAdapter/NodeAdapter.',\n );\n }\n\n if (!isBrowserRuntime) {\n return;\n }\n\n if (adapterType === 'node') {\n throw new Error(\n 'HTTPClient Node adapter is not supported in browser environments.',\n );\n }\n\n // MockAdapter is intentionally allowed in browser runtimes: it is an\n // in-memory test adapter, so cookie jars and redirect following are local\n // simulation features rather than forbidden browser networking controls.\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.cookieJar) {\n throw new Error(\n `HTTPClient cookieJar is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers manage cookies automatically.`,\n );\n }\n\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.userAgent) {\n throw new Error(\n `HTTPClient userAgent is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers do not allow overriding the User-Agent header.`,\n );\n }\n\n if (adapterType === 'fetch' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with FetchAdapter in browser environments. Set followRedirects: false or use a server runtime.',\n );\n }\n}\n\nfunction requiresAbsoluteBaseURL(\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): boolean {\n if (adapterType === 'node' || adapterType === 'mock') {\n return true;\n }\n\n if (adapterType === 'fetch' && !isBrowserRuntime) {\n return true;\n }\n\n return false;\n}\n\nfunction hasXMLHttpRequestGlobal(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof (globalThis as { XMLHttpRequest?: unknown }).XMLHttpRequest ===\n 'function'\n );\n}\n\n/**\n * Converts a Headers object (from fetch) into AdapterResponse headers.\n * `set-cookie` is extracted as `string[]` via `getSetCookie()` — the Fetch API\n * would otherwise incorrectly comma-join multiple Set-Cookie values.\n * All other headers are extracted as plain strings.\n */\nexport function extractFetchHeaders(\n headers: Headers,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of headers.entries()) {\n const lower = key.toLowerCase();\n\n if (lower !== 'set-cookie') {\n result[lower] = value;\n }\n }\n\n // Use getSetCookie() when available (Bun, Node 18.14+, modern browsers)\n if (typeof headers.getSetCookie === 'function') {\n const setCookies = headers.getSetCookie();\n\n if (setCookies.length > 0) {\n result['set-cookie'] = setCookies;\n }\n } else {\n // Fallback: headers.get() comma-joins — split on ', ' is unreliable for\n // cookies but better than nothing on older runtimes\n const raw = headers.get('set-cookie');\n\n if (raw) {\n result['set-cookie'] = [raw];\n }\n }\n\n return result;\n}\n\n/**\n * Lowercases all keys on adapter/response header objects. `HTTPClient` runs\n * this on each adapter response before {@link CookieJar.processResponseHeaders}.\n * The jar also normalizes so the same shapes work when feeding headers directly.\n *\n * - Non–`set-cookie` values: if an array appears (unexpected), the first\n * element is kept when read via {@link scalarHeader}.\n * - `set-cookie`: stored as `string[]` — each array entry is one full\n * `Set-Cookie` header line (one cookie). A single string value becomes a\n * one-element array. If the same header appears under keys that differ only\n * by case, those lines are appended in the order they appear on the input\n * object.\n */\nexport function normalizeAdapterResponseHeaders(\n headers: Record<string, string | string[]>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n\n if (lower === 'set-cookie') {\n const chunk = Array.isArray(value) ? value : [value];\n const existing = result[lower];\n\n if (existing === undefined) {\n result[lower] = chunk;\n } else {\n const existingLines = Array.isArray(existing) ? existing : [existing];\n result[lower] = [...existingLines, ...chunk];\n }\n } else {\n result[lower] = Array.isArray(value) ? (value[0] ?? '') : value;\n }\n }\n\n return result;\n}\n\n/**\n * Reads a single-valued header when keys are already lowercase (e.g. after\n * {@link mergeHeaders} on requests or {@link normalizeAdapterResponseHeaders}\n * on responses). If the stored value is `string[]`, returns the first entry.\n */\nexport function scalarHeader(\n headers: Record<string, string | string[]>,\n lowercaseName: string,\n): string | undefined {\n const v = headers[lowercaseName];\n\n if (v === undefined) {\n return undefined;\n }\n\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Resolves a redirect target from response headers when the adapter can\n * observe a redirect response but the client may not follow it itself.\n */\nexport function resolveDetectedRedirectURL(\n requestURL: string,\n status: number,\n headers: Record<string, string | string[]>,\n baseURL?: string,\n): string | undefined {\n if (![301, 302, 303, 307, 308].includes(status)) {\n return undefined;\n }\n\n const location = scalarHeader(headers, 'location');\n\n if (!location) {\n return undefined;\n }\n\n try {\n const absoluteRequestURL = resolveAbsoluteURL(requestURL, baseURL);\n return new URL(location, absoluteRequestURL).toString();\n } catch {\n return location;\n }\n}\n\nexport function assertValidBaseURL(\n baseURL: string,\n fieldName = 'baseURL',\n): void {\n try {\n const url = new URL(baseURL);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('unsupported protocol');\n }\n } catch {\n throw new Error(\n `HTTPClient ${fieldName} must be an absolute http(s) URL (for example \"https://api.example.com\").`,\n );\n }\n}\n\n/**\n * Detects whether the current runtime looks like a browser environment.\n */\nexport function isBrowserEnvironment(): boolean {\n if (typeof globalThis === 'undefined') {\n return false;\n }\n\n if ('window' in globalThis && 'document' in globalThis) {\n return true;\n }\n\n const workerGlobalScope = (\n globalThis as {\n WorkerGlobalScope?: abstract new (...args: never[]) => unknown;\n }\n ).WorkerGlobalScope;\n\n if (\n typeof workerGlobalScope === 'function' &&\n (globalThis as { self?: unknown }).self instanceof workerGlobalScope\n ) {\n return true;\n }\n\n const constructorName = globalThis.constructor?.name;\n\n return (\n !('window' in globalThis) &&\n !('document' in globalThis) &&\n typeof constructorName === 'string' &&\n constructorName.endsWith('WorkerGlobalScope')\n );\n}\n\n/**\n * Serializes the request body and returns the body + inferred content-type.\n * If `formData` is provided, it takes precedence over `body`.\n */\nexport function serializeBody(body: unknown): {\n body: string | Uint8Array | FormData | null;\n contentType: string | null;\n} {\n assertSupportedRequestBody(body);\n\n if (body instanceof FormData) {\n return { body, contentType: null }; // browser/runtime sets multipart boundary automatically\n } else if (body === undefined || body === null) {\n return { body: null, contentType: null };\n } else if (typeof body === 'string') {\n return { body, contentType: 'text/plain; charset=utf-8' };\n } else if (body instanceof Uint8Array) {\n return { body, contentType: 'application/octet-stream' };\n } else if (Array.isArray(body) || isPlainJSONBodyObject(body)) {\n return {\n body: JSON.stringify(body),\n contentType: 'application/json; charset=utf-8',\n };\n } else {\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n }\n}\n\nexport function assertSupportedRequestBody(body: unknown): void {\n if (\n body === undefined ||\n body === null ||\n typeof body === 'string' ||\n body instanceof Uint8Array ||\n body instanceof FormData ||\n Array.isArray(body) ||\n isPlainJSONBodyObject(body)\n ) {\n return;\n }\n\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n}\n\nexport function isPlainJSONBodyObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n\n const prototype = Reflect.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/**\n * Extracts the hostname from a URL string. Returns empty string on failure.\n */\nexport function extractHostname(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/**\n * Wildcard hostname matching backed by {@link matchesWildcardDomain}.\n *\n * - `*` — global wildcard, matches any valid hostname including apex domains\n * - `*.example.com` — matches exactly one subdomain label (`api.example.com`) but not deeper levels or the apex\n * - `**.example.com` — matches one or more subdomain labels (`api.example.com`, `a.b.example.com`) but not the apex\n * - Exact patterns (no `*`) — normalized comparison, case-insensitive\n * - PSL tail guard active: `*.com`, `**.co.uk`, etc. never match\n * - Pseudo-TLD suffix wildcards are rejected just like PSL tails (`*.localhost`, `*.local`, etc.)\n */\nexport function matchesHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.includes('*')) {\n return matchesWildcardDomain(hostname, pattern);\n }\n\n const normalizedHostname = normalizeDomain(hostname);\n const normalizedPattern = normalizeDomain(pattern);\n return normalizedHostname !== '' && normalizedHostname === normalizedPattern;\n}\n\n/**\n * Checks whether a dot-path key exists in a nested object.\n * Arrays are not traversed — only plain objects at each segment.\n */\nfunction hasNestedKey(obj: Record<string, unknown>, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n return false;\n }\n\n if (!(part in (current as Record<string, unknown>))) {\n return false;\n }\n\n current = (current as Record<string, unknown>)[part];\n }\n\n return true;\n}\n\nfunction normalizeMimeType(value: string): string {\n return value.split(';', 1)[0].trim().toLowerCase();\n}\n\nfunction matchesContentTypePattern(\n actualHeader: string,\n pattern: string,\n): boolean {\n const actual = normalizeMimeType(actualHeader);\n const expected = normalizeMimeType(pattern);\n\n if (!actual || !expected) {\n return false;\n }\n\n if (expected.endsWith('/*')) {\n const expectedType = expected.slice(0, -2);\n const slashIndex = actual.indexOf('/');\n\n if (slashIndex === -1) {\n return false;\n }\n\n return actual.slice(0, slashIndex) === expectedType;\n }\n\n return actual === expected;\n}\n\n/**\n * Tests whether a request context matches an interceptor/observer filter.\n *\n * Each filter field is optional — omitting it skips that check entirely.\n * All specified fields must match for the function to return true.\n * Within each field, values are matched with OR logic (any one match is sufficient).\n *\n * - `phases`: **OR** allowlist on `phaseType` ({@link RequestPhaseName}). Skipped when\n * `filter.phases` is omitted or empty.\n * - `statusCodes`: skipped if `context.status` is absent.\n * - `methods`: skipped if `context.method` is absent.\n * - `hosts`: supports exact hostnames and wildcard patterns. `*.example.com` matches\n * exactly one subdomain label; `**.example.com` matches any depth. Neither matches the\n * apex — list it explicitly. PSL tail guard prevents `*.com`-style patterns. `*` is a\n * global wildcard that matches any valid hostname. Skipped if `context.requestURL` is absent.\n * - `schemes`: `'http'` or `'https'`. `requestURL` is absolute whenever the\n * request could be resolved before dispatch. For `MockAdapter`, path-only\n * requests without a client `baseURL` are materialized as `http://localhost/...`;\n * browser adapters fall back to `window.location`, and the Node adapter requires\n * absolute URLs. Skipped only when `requestURL` is absent.\n * - `bodyContainsKeys`: supports dot paths (e.g. `data.results`). Each segment in\n * the path must resolve to a plain object for traversal to continue — the final\n * value can be anything (array, string, null, etc). Array indexing is not supported.\n * Skipped when `kind` is `'error'`.\n */\nexport function matchesFilter(\n filter: {\n statusCodes?: number[];\n methods?: string[];\n bodyContainsKeys?: string[];\n hosts?: string[];\n schemes?: ('http' | 'https')[];\n phases?: RequestPhaseName[];\n contentTypes?: ContentType[];\n contentTypeHeaders?: string[];\n },\n context: {\n status?: number;\n method?: string;\n body?: unknown;\n requestURL?: string;\n contentType?: ContentType;\n contentTypeHeader?: string;\n },\n phaseType: RequestPhaseName,\n kind: 'request' | 'response' | 'error',\n): boolean {\n if (\n filter.phases &&\n filter.phases.length > 0 &&\n !filter.phases.includes(phaseType)\n ) {\n return false;\n }\n\n if (filter.statusCodes && context.status !== undefined) {\n if (!filter.statusCodes.includes(context.status)) {\n return false;\n }\n }\n\n if (filter.methods && context.method) {\n if (!filter.methods.includes(context.method)) {\n return false;\n }\n }\n\n if (filter.contentTypes && filter.contentTypes.length > 0) {\n if (\n !context.contentType ||\n !filter.contentTypes.includes(context.contentType)\n ) {\n return false;\n }\n }\n\n if (filter.contentTypeHeaders && filter.contentTypeHeaders.length > 0) {\n if (\n !context.contentTypeHeader ||\n !filter.contentTypeHeaders.some((pattern) =>\n matchesContentTypePattern(context.contentTypeHeader as string, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (\n kind !== 'error' &&\n filter.bodyContainsKeys &&\n filter.bodyContainsKeys.length > 0\n ) {\n if (\n !context.body ||\n typeof context.body !== 'object' ||\n Array.isArray(context.body)\n ) {\n return false;\n }\n\n const body = context.body as Record<string, unknown>;\n\n if (!filter.bodyContainsKeys.some((k) => hasNestedKey(body, k))) {\n return false;\n }\n }\n\n if (filter.hosts && context.requestURL) {\n const hostname = extractHostname(context.requestURL);\n\n if (\n !filter.hosts.some((pattern: string) =>\n matchesHostPattern(hostname, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (filter.schemes && filter.schemes.length > 0 && context.requestURL) {\n let scheme: 'http' | 'https' | null = null;\n\n try {\n const parsedScheme = new URL(context.requestURL).protocol.replace(\n ':',\n '',\n );\n\n if (parsedScheme === 'http' || parsedScheme === 'https') {\n scheme = parsedScheme;\n }\n } catch {\n scheme = null;\n }\n\n if (!scheme || !filter.schemes.includes(scheme)) {\n return false;\n }\n }\n\n return true;\n}\n","import { XHR_BROWSER_TIMEOUT_FLAG } from '../consts';\nimport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n} from '../types';\nimport { resolveAbsoluteURLForRuntime } from '../utils';\n\n/**\n * XHR-based adapter for environments that expose `XMLHttpRequest`. Primary\n * advantage over FetchAdapter is real per-chunk upload and download progress\n * via `xhr.upload.onprogress` / `xhr.onprogress`. FetchAdapter only fires 0%\n * and 100% because the Fetch API has no streaming upload and requires\n * buffering the full response to read body bytes.\n *\n * XHR constraints compared to FetchAdapter and NodeAdapter:\n * - `followRedirects: false` is required — XHR offers no opt-out from\n * automatic redirect following, so individual hops cannot be observed or\n * controlled. Redirect following is unsupported and treated as an error.\n * - In browser runtimes, cookies, CORS, and restricted headers (e.g. Cookie,\n * User-Agent) are browser-managed; `cookieJar` must not be passed there.\n */\nexport class XHRAdapter implements HTTPAdapter {\n public getType(): AdapterType {\n return 'xhr';\n }\n\n public send(request: AdapterRequest): Promise<AdapterResponse> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n // responseType 'arraybuffer' gives us a raw ArrayBuffer on load,\n // consistent with how FetchAdapter and NodeAdapter deliver body bytes.\n xhr.open(request.method, request.requestURL);\n xhr.responseType = 'arraybuffer';\n\n // Timeout is managed by the client via the abort signal — the client's\n // per-attempt timer fires AbortController.abort(), which propagates to\n // xhr.abort() through the signal listener below. We disable XHR's own\n // timeout mechanism (0 = no timeout) so the client retains full control.\n // The 'timeout' event listener below is kept as a defensive fallback in\n // case a browser fires it anyway (e.g. a hard-coded internal limit).\n xhr.timeout = 0;\n\n // --- Request headers ---\n //\n // Calling setRequestHeader multiple times for the same key causes XHR to\n // combine values with \", \" per spec — which is correct for all headers\n // the browser allows scripts to set. Cookie is a forbidden header name\n // and is silently dropped by the browser regardless; the browser manages\n // cookies on its own.\n for (const [key, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) {\n for (const v of value) {\n xhr.setRequestHeader(key, v);\n }\n } else {\n xhr.setRequestHeader(key, value);\n }\n }\n\n // --- Abort signal ---\n //\n // Check for pre-aborted signal before calling xhr.send — if we called\n // send first and then aborted, the abort event fires asynchronously and\n // we'd resolve the promise rather than reject it with an AbortError.\n if (request.signal) {\n if (request.signal.aborted) {\n reject(new DOMException('Request aborted', 'AbortError'));\n return;\n }\n\n request.signal.addEventListener(\n 'abort',\n () => {\n xhr.abort();\n },\n // once: true — the XHR is already done after the first abort, no\n // need to keep the listener alive and risk a second call.\n { once: true },\n );\n }\n\n // --- Upload progress ---\n\n // Fire initial 0% upload progress before any bytes leave the browser,\n // mirroring the FetchAdapter pattern so callers see a consistent first\n // event regardless of adapter.\n request.onUploadProgress?.({ loaded: 0, total: 0, progress: 0 });\n\n // Real per-chunk upload progress — the main advantage over FetchAdapter,\n // which has no streaming upload and can only fire 0% then 100%.\n // Deduplication guard — upload.progress and upload.load can both report\n // 100% (see upload.load listener below for details).\n let didFireUpload100 = false;\n let uploadedBytes = 0;\n let uploadTotalBytes = 0;\n\n xhr.upload.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireUpload100 = true;\n }\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(uploadTotalBytes, event.total);\n\n request.onUploadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // 100% upload fires as soon as all bytes are sent, always before xhr.load\n // per spec. Skip if upload.progress already reported 100% to avoid a\n // duplicate event. We still track whether this fired so xhr.load can use\n // it as a fallback for environments that skip upload.load entirely.\n let didUploadComplete = false;\n\n xhr.upload.addEventListener('load', (event) => {\n didUploadComplete = true;\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(\n uploadTotalBytes,\n event.total || event.loaded,\n );\n\n if (!didFireUpload100) {\n const finalLoaded = uploadedBytes > 0 ? uploadedBytes : 1;\n const finalTotal = uploadTotalBytes > 0 ? uploadTotalBytes : 1;\n request.onUploadProgress?.({\n loaded: finalLoaded,\n total: finalTotal,\n progress: 1,\n });\n }\n });\n\n // --- Download progress ---\n\n // Real per-chunk download progress. Same advantage over FetchAdapter:\n // FetchAdapter buffers the full response body before firing any progress,\n // so it can only ever report 0% then 100%.\n // Deduplication guard — when Content-Length is known and the final\n // progress chunk reaches 100%, xhr.load would otherwise fire it again.\n let didFireDownload100 = false;\n let downloadedBytes = 0;\n let downloadTotalBytes = 0;\n\n xhr.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireDownload100 = true;\n }\n\n downloadedBytes = Math.max(downloadedBytes, event.loaded);\n downloadTotalBytes = Math.max(downloadTotalBytes, event.total);\n\n request.onDownloadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // --- Load (success) ---\n\n xhr.addEventListener('load', () => {\n // Detect browser-followed redirects.\n //\n // In a browser, FetchAdapter uses `redirect: 'manual'` which yields an\n // opaqueredirect response (status 0) — redirects are intercepted before\n // they happen. XHR has no equivalent opt-out; the browser always follows\n // redirects automatically. We detect them after-the-fact by comparing\n // xhr.responseURL (the final URL after all hops) to the original URL.\n //\n // Both browser adapters surface the same signal: status 0 +\n // wasRedirectDetected, which routes through HTTPClient's\n // redirect_disabled error path so callers get a consistent isFailed\n // response regardless of adapter.\n if (\n xhr.responseURL &&\n didBrowserFollowRedirect(xhr.responseURL, request.requestURL)\n ) {\n // The browser completed the transport and surfaced the final URL even\n // though the client will treat the result as redirect_disabled, so\n // emit terminal progress before returning the synthetic redirect\n // response.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: downloadedBytes,\n total: downloadTotalBytes,\n progress: 1,\n });\n }\n\n resolve({\n status: 0,\n wasRedirectDetected: true,\n // XHR exposes the post-redirect final URL via responseURL. Browser\n // fetch opaque redirects do not, so this is intentionally\n // adapter-specific and surfaced separately from requestURL.\n detectedRedirectURL: xhr.responseURL,\n headers: {},\n body: null,\n });\n return;\n }\n\n // Fallback: upload.load didn't fire (no request body, or the browser\n // skipped the event). Ensure callers always see a 100% upload event,\n // unless upload.progress already reported it.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n const body = readResponseBody(request.method, xhr);\n\n // Final 100% download progress — skip if a progress event already\n // fired exactly 100% (Content-Length known and final chunk completed it).\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: body?.length ?? 0,\n total: body?.length ?? 0,\n progress: 1,\n });\n }\n\n resolve({\n status: xhr.status,\n headers: parseXHRResponseHeaders(xhr.getAllResponseHeaders()),\n body,\n });\n });\n\n // --- Error / timeout / abort ---\n\n // The error event fires for network-level failures (DNS failure, refused\n // connection, CORS rejection). It never fires for HTTP error status codes\n // (4xx, 5xx) — those arrive on the load event with a real status.\n xhr.addEventListener('error', () => {\n // Neither replay signal is set here, on purpose. This adapter can never\n // prove non-delivery: upload progress is suppressed for cross-origin\n // requests CORS does not grant access to, and those are still delivered —\n // the browser blocks the response, not the request. The omission is what\n // keeps a `POST` from being replayed by default.\n //\n // `isRetryable: false` does not stand in for it: that flag blocks every\n // method, so it would stop retrying an idempotent `PUT` after an ordinary\n // network error and override `retryNonIdempotentMethods`.\n resolve({\n status: 0,\n isTransportError: true,\n headers: {},\n body: null,\n errorCause: new Error('XHR network error'),\n });\n });\n\n // Defensive fallback: fires if the browser has a hard-coded internal\n // timeout limit (xhr.timeout is 0 so we never set one ourselves). Mark\n // the error so HTTPClient classifies it as a timeout (retryable) rather\n // than an unexpected abort (non-retryable cancel).\n xhr.addEventListener('timeout', () => {\n reject(\n Object.assign(new DOMException('Request timed out', 'AbortError'), {\n [XHR_BROWSER_TIMEOUT_FLAG]: true,\n }),\n );\n });\n\n xhr.addEventListener('abort', () => {\n reject(new DOMException('Request aborted', 'AbortError'));\n });\n\n xhr.send(prepareBody(request.body));\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the response body as a Uint8Array, or null for response types that\n * carry no body (HEAD, 204 No Content, 304 Not Modified).\n */\nfunction readResponseBody(\n method: string,\n xhr: XMLHttpRequest,\n): Uint8Array | null {\n if (method === 'HEAD' || xhr.status === 204 || xhr.status === 304) {\n return null;\n }\n\n if (xhr.response instanceof ArrayBuffer) {\n return new Uint8Array(xhr.response);\n }\n\n return null;\n}\n\n/**\n * Converts the adapter request body to a value accepted by `xhr.send()`.\n * `string`, `Uint8Array` (BufferSource), and `FormData` are all valid\n * `XMLHttpRequestBodyInit` values — the cast is safe for the body types\n * the client produces.\n */\nfunction prepareBody(\n body: AdapterRequest['body'],\n): XMLHttpRequestBodyInit | null {\n if (body === null) {\n return null;\n }\n\n return body as XMLHttpRequestBodyInit;\n}\n\n/**\n * Parses the raw header string from `xhr.getAllResponseHeaders()` into a\n * lowercase-keyed record.\n *\n * `getAllResponseHeaders()` returns CRLF-delimited `name: value` lines. When\n * a server sends multiple headers with the same name the browser combines them\n * into a single comma-joined line for most headers, but emits each `Set-Cookie`\n * value as its own line (per spec) to avoid ambiguity with the comma in cookie\n * values. Those are collected here as `string[]` to match the\n * `AdapterResponse.headers` contract.\n *\n * Note: browsers unconditionally block `Set-Cookie` and `Set-Cookie2` from\n * `getAllResponseHeaders()` per the XHR spec, so the `set-cookie` array\n * branch below is effectively unreachable in a real browser — it exists to\n * satisfy the shared `AdapterResponse` type contract.\n */\nfunction parseXHRResponseHeaders(\n raw: string,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n if (!raw) {\n return result;\n }\n\n for (const line of raw.split('\\r\\n')) {\n // Lines are `Name: value` pairs separated by the first colon.\n // indexOf is used (not split) so colons in the value are preserved.\n const colonIndex = line.indexOf(':');\n\n if (colonIndex < 0) {\n // No colon — malformed or trailing empty line; skip\n continue;\n }\n\n // Lowercase to normalize across servers (header names are case-insensitive)\n const key = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n\n if (!key) {\n // Colon at position 0 — no name; skip\n continue;\n }\n\n if (key === 'set-cookie') {\n // Each Set-Cookie directive arrives as its own line — collect into an\n // array so callers never need to split on commas (which are valid inside\n // cookie values). Guarded by the XHR spec in standard browsers, but kept\n // for correctness in legacy environments or platforms with non-standard\n // XHR implementations.\n const existing = result['set-cookie'];\n\n if (existing === undefined) {\n result['set-cookie'] = [value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result['set-cookie'] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Compares URLs as browsers evaluate request destinations:\n * - strips hash fragments (not sent over HTTP)\n * - relies on URL normalization for equivalent forms\n * (default ports, dot segments, encoding normalization, etc.)\n */\nfunction didBrowserFollowRedirect(\n responseURL: string,\n requestURL: string,\n): boolean {\n try {\n const normalizedResponse = new URL(responseURL);\n normalizedResponse.hash = '';\n\n const normalizedRequest = new URL(\n resolveAbsoluteURLForRuntime(requestURL, undefined, true),\n normalizedResponse.href,\n );\n normalizedRequest.hash = '';\n\n return normalizedResponse.href !== normalizedRequest.href;\n } catch {\n // Fallback for non-URL inputs: preserve prior behavior.\n return responseURL !== requestURL;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
- import { H as HTTPAdapter, A as AdapterType, a as AdapterRequest, b as AdapterResponse } from '../../types-6G59m8U9.cjs';
2
- export { c as AdapterProgressEvent, d as HTTPMethod } from '../../types-6G59m8U9.cjs';
1
+ import { H as HTTPAdapter, A as AdapterType, a as AdapterRequest, b as AdapterResponse } from '../../types-BFvpJPMt.cjs';
2
+ export { c as AdapterProgressEvent, d as HTTPMethod } from '../../types-BFvpJPMt.cjs';
3
3
  import '../../types-BW0vnSzd.cjs';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { H as HTTPAdapter, A as AdapterType, a as AdapterRequest, b as AdapterResponse } from '../../types-CG10j6Lc.js';
2
- export { c as AdapterProgressEvent, d as HTTPMethod } from '../../types-CG10j6Lc.js';
1
+ import { H as HTTPAdapter, A as AdapterType, a as AdapterRequest, b as AdapterResponse } from '../../types-BONtIhQ5.js';
2
+ export { c as AdapterProgressEvent, d as HTTPMethod } from '../../types-BONtIhQ5.js';
3
3
  import '../../types-BW0vnSzd.js';
4
4
 
5
5
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/http-client/consts.ts","../../../src/lib/http-client/utils.ts","../../../src/lib/http-client/adapters/xhr-adapter.ts"],"sourcesContent":["import type { HTTPMethod } from './types';\n\n/**\n * HTTP responses that are plausibly transient and worth retrying when a retry\n * policy is explicitly enabled.\n *\n * `status === 0` is included on purpose because browser/XHR-style adapters can\n * surface \"no real HTTP response\" that way when the network is unavailable or\n * the request otherwise fails before a normal status code is received.\n */\nexport const RETRYABLE_STATUS_CODES: ReadonlySet<number> = new Set([\n // 0: Browser/XHR-style \"no response\" status.\n 0,\n\n // 408 Request Timeout\n 408,\n // 429 Too Many Requests\n 429,\n\n // 500 Internal Server Error\n 500,\n // 502 Bad Gateway\n 502,\n // 503 Service Unavailable\n 503,\n // 504 Gateway Timeout\n 504,\n\n // 507 Insufficient Storage\n 507,\n // 509 Bandwidth Limit Exceeded (non-standard)\n 509,\n // 520 Unknown Error (Cloudflare)\n 520,\n // 521 Web Server Is Down (Cloudflare)\n 521,\n // 522 Connection Timed Out (Cloudflare)\n 522,\n // 523 Origin Is Unreachable (Cloudflare)\n 523,\n // 524 A Timeout Occurred (Cloudflare)\n 524,\n // 598 Network Read Timeout Error (non-standard)\n 598,\n // 599 Network Connect Timeout Error (non-standard)\n 599,\n]);\n\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-local-client-request-id';\n\nexport const DEFAULT_REQUEST_ATTEMPT_HEADER = 'x-local-client-request-attempt';\n\nexport const DEFAULT_USER_AGENT = 'lifecycleion-http-client';\n\nexport const NON_RETRYABLE_HTTP_CLIENT_CALLBACK_ERROR_FLAG =\n '_lifecycleion_non_retryable_http_client_callback_error';\n\nexport const STREAM_FACTORY_ERROR_FLAG = '_lifecycleion_stream_factory_error';\n\n/**\n * Attached to the AbortError thrown when a StreamResponseFactory returns null\n * or `{ cancel: true, reason? }`. The value is the reason string if provided,\n * or `true` if the factory cancelled without a reason. Lets HTTPClient surface\n * the reason on HTTPClientError.cancelReason.\n */\nexport const STREAM_FACTORY_CANCEL_KEY =\n '_lifecycleion_stream_factory_cancel_reason';\n\nexport const RESPONSE_STREAM_ABORT_FLAG = '_lifecycleion_response_stream_abort';\n\n/**\n * Set on the AbortError thrown by XHRAdapter's defensive `timeout` event\n * listener. Lets HTTPClient classify the error as a timeout (retryable) rather\n * than an unexpected abort (non-retryable cancel).\n */\nexport const XHR_BROWSER_TIMEOUT_FLAG = '_lifecycleion_xhr_browser_timeout';\n\nexport const HTTP_METHODS: ReadonlyArray<HTTPMethod> = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n];\n\n/**\n * Exact-match request headers that browsers either forbid outright or do not\n * let this client set reliably via plain Fetch/XHR headers.\n *\n * Prefix-based rules like `proxy-*` and `sec-*` are handled in `header-utils.ts`.\n */\nexport const BROWSER_RESTRICTED_HEADERS: ReadonlySet<string> = new Set([\n // Encoding / CORS negotiation headers controlled by the browser.\n 'accept-charset',\n 'accept-encoding',\n 'access-control-request-headers',\n 'access-control-request-method',\n 'access-control-request-private-network',\n\n // Connection-level transport headers.\n 'connection',\n 'content-length',\n 'date',\n 'expect',\n 'host',\n 'keep-alive',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'via',\n\n // Browser-managed request context / privacy headers.\n 'cookie',\n 'dnt',\n 'origin',\n 'referer',\n 'set-cookie',\n 'user-agent',\n]);\n\nexport const BROWSER_RESTRICTED_HEADER_PREFIXES: ReadonlyArray<string> = [\n 'proxy-',\n 'sec-',\n];\n\n/**\n * Headers that can tunnel the real method through POST. Browsers block these\n * when they try to smuggle forbidden transport methods.\n */\nexport const BROWSER_METHOD_OVERRIDE_HEADER_NAMES: ReadonlySet<string> =\n new Set(['x-http-method', 'x-http-method-override', 'x-method-override']);\n\n/**\n * Methods that browsers do not allow request headers to tunnel via the\n * override headers above.\n */\nexport const BROWSER_FORBIDDEN_METHOD_OVERRIDE_VALUES: ReadonlySet<string> =\n new Set(['connect', 'trace', 'track']);\n\nexport const DEFAULT_MAX_REDIRECTS = 5;\n\n/**\n * Redirect responses that carry a follow-up `Location` hop. `300` and `304`\n * are excluded because they do not represent an automatic redirect here.\n */\nexport const REDIRECT_STATUS_CODES: ReadonlySet<number> = new Set([\n // 301 Moved Permanently\n 301,\n // 302 Found\n 302,\n\n // 303 See Other\n 303,\n\n // 307 Temporary Redirect\n 307,\n // 308 Permanent Redirect\n 308,\n]);\n","import qs from 'qs';\nimport {\n matchesWildcardDomain,\n normalizeDomain,\n} from '../domain-utils/domain-utils';\nimport type {\n ContentType,\n RequestPhaseName,\n HTTPClientConfig,\n AdapterType,\n} from './types';\n\n/**\n * If `path` is an absolute HTTP(S) URL, returns its canonical `href` (normalized\n * scheme/host casing). Otherwise `null`. Protocol-relative `//host` is handled\n * separately in `buildURL`.\n *\n * Rules differ from `resolveAbsoluteURL` (which accepts any absolute scheme).\n * One parse here per request is negligible next to network I/O.\n */\nfunction tryAbsoluteWebHref(path: string): string | null {\n if (!path || path.startsWith('//')) {\n return null;\n }\n\n try {\n const u = new URL(path);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n return u.href;\n }\n } catch {\n // not parseable as absolute\n }\n\n return null;\n}\n\n/**\n * Builds a request URL string from `baseURL`, `path`, and optional query params\n * (`qs` — nested objects and arrays supported).\n *\n * **Relative paths (usual case)** — When `baseURL` is set and `path` is not\n * absolute, `path` is joined to `baseURL` (leading slash normalized). Example:\n * `baseURL: https://api.test`, `path: /v1/users` → `https://api.test/v1/users`.\n *\n * **Absolute / protocol-relative `path` (escape hatch)** — If `path` is a full\n * `http:` or `https:` URL, it is **not** prefixed with `baseURL` (after\n * normalization via `URL#href`). The same applies to protocol-relative URLs\n * (`//cdn.example/x`): they are left for {@link resolveAbsoluteURL} to resolve\n * using the client `baseURL`’s scheme. Use this for one-off cross-origin calls,\n * CDN assets, or URLs returned by APIs; for strict per-origin clients, prefer\n * relative paths and a dedicated client or `HTTPClient.createSubClient()` per\n * origin.\n */\nexport function buildURL(\n baseURL: string | undefined,\n path: string,\n params?: Record<string, unknown>,\n): string {\n let url: string;\n\n const absoluteHref = tryAbsoluteWebHref(path);\n\n if (baseURL && absoluteHref === null && !path.startsWith('//')) {\n // Avoid double slashes when joining base + path\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const p = path.startsWith('/') ? path : `/${path}`;\n url = `${base}${p}`;\n } else if (absoluteHref !== null) {\n url = absoluteHref;\n } else {\n url = path;\n }\n\n if (params && Object.keys(params).length > 0) {\n const [urlWithoutHash, hash = ''] = url.split('#', 2);\n const queryStartIndex = urlWithoutHash.indexOf('?');\n\n if (queryStartIndex === -1) {\n const queryString = qs.stringify(params, { addQueryPrefix: true });\n url = `${urlWithoutHash}${queryString}${hash ? `#${hash}` : ''}`;\n } else {\n const basePath = urlWithoutHash.slice(0, queryStartIndex);\n const existingQuery = urlWithoutHash.slice(queryStartIndex + 1);\n // Fragments are preserved only as part of the caller's URL string.\n // They are not transmitted in HTTP requests, but keeping them intact\n // makes buildURL safer as a general-purpose URL composition helper.\n const mergedParams = {\n ...qs.parse(existingQuery),\n ...params,\n };\n\n const queryString = qs.stringify(mergedParams, { addQueryPrefix: true });\n url = `${basePath}${queryString}${hash ? `#${hash}` : ''}`;\n }\n }\n\n return url;\n}\n\n/**\n * Best-effort absolute URL for logging, redirects, and hop metadata.\n *\n * - If `url` parses as an absolute URL (has a scheme), returns normalized `href`.\n * - Otherwise, when `baseURL` is set, resolves `url` against it (path-relative,\n * same-host relative, protocol-relative `//host`, query-only, etc.).\n * - If neither works, returns `url` unchanged (callers without `baseURL` may still\n * see path-only strings).\n */\nexport function resolveAbsoluteURL(url: string, baseURL?: string): string {\n if (!url) {\n return url;\n }\n\n try {\n return new URL(url).href;\n } catch {\n // Not a standalone absolute URL\n }\n\n if (baseURL) {\n try {\n const base = baseURL.endsWith('/') ? baseURL : `${baseURL}/`;\n return new URL(url, base).href;\n } catch {\n // fall through\n }\n }\n\n return url;\n}\n\n/**\n * Browser-aware absolute URL resolution used by HTTPClient before interceptors\n * and adapter dispatch. Starts with normal baseURL resolution, then falls back\n * to the current page/worker location when running in a browser-like runtime.\n */\nexport function resolveAbsoluteURLForRuntime(\n url: string,\n baseURL: string | undefined,\n isBrowserRuntime: boolean,\n): string {\n const resolved = resolveAbsoluteURL(url, baseURL);\n\n if (\n !isBrowserRuntime ||\n resolved.startsWith('http://') ||\n resolved.startsWith('https://')\n ) {\n return resolved;\n }\n\n const browserBase = getBrowserResolutionBase();\n\n if (!browserBase) {\n return resolved;\n }\n\n return resolveAbsoluteURL(resolved, browserBase);\n}\n\nfunction getBrowserResolutionBase(): string | undefined {\n if (\n typeof document !== 'undefined' &&\n typeof document.baseURI === 'string' &&\n document.baseURI\n ) {\n return document.baseURI;\n }\n\n if (\n typeof window !== 'undefined' &&\n window.location &&\n typeof window.location.href === 'string' &&\n window.location.href\n ) {\n return window.location.href;\n }\n\n const globalLocation = (globalThis as { location?: { href?: unknown } })\n .location;\n\n if (\n globalLocation &&\n typeof globalLocation.href === 'string' &&\n globalLocation.href\n ) {\n return globalLocation.href;\n }\n\n const selfLocation = (\n globalThis as { self?: { location?: { href?: unknown } } }\n ).self?.location;\n\n if (\n selfLocation &&\n typeof selfLocation.href === 'string' &&\n selfLocation.href\n ) {\n return selfLocation.href;\n }\n\n return undefined;\n}\n\n/**\n * Normalizes header keys to lowercase.\n */\nexport function normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = value;\n }\n\n return result;\n}\n\n/**\n * Merges multiple request-header objects, normalizing keys to lowercase.\n * Later objects win on conflict. Array values replace earlier scalars/arrays\n * wholesale, and single-item arrays are collapsed back to a plain string.\n */\nexport function mergeHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? normalizeMergedHeaderArray(value)\n : String(value);\n }\n }\n\n return result;\n}\n\nfunction normalizeMergedHeaderArray(value: string[]): string | string[] {\n const normalized = value.map((item) => String(item));\n return normalized.length === 1 ? normalized[0] : normalized;\n}\n\nexport function mergeObservedHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? value.map((item) => String(item))\n : String(value);\n }\n }\n\n return result;\n}\n\n/**\n * Parses the Content-Type header into a ContentType enum value.\n */\nexport function parseContentType(\n contentTypeHeader: string | undefined,\n): ContentType {\n if (!contentTypeHeader) {\n return 'binary';\n } else {\n const lower = contentTypeHeader.trim().toLowerCase();\n\n if (lower.includes('application/json') || lower.includes('+json')) {\n return 'json';\n } else if (lower.startsWith('text/')) {\n return 'text';\n } else if (lower.includes('application/x-www-form-urlencoded')) {\n return 'text';\n } else {\n return 'binary';\n }\n }\n}\n\n/**\n * Validates adapter/runtime combinations and redirect config before a client\n * is constructed, so unsupported browser-only/server-only options fail fast\n * with clear errors instead of surfacing later during request dispatch.\n */\nexport function assertSupportedAdapterRuntimeAndConfig(\n config: HTTPClientConfig,\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): void {\n if (\n config.baseURL !== undefined &&\n requiresAbsoluteBaseURL(adapterType, isBrowserRuntime)\n ) {\n assertValidBaseURL(config.baseURL);\n }\n\n if (config.maxRedirects !== undefined && config.followRedirects !== true) {\n throw new Error('HTTPClient maxRedirects requires followRedirects: true.');\n }\n\n if (\n config.followRedirects === true &&\n config.maxRedirects !== undefined &&\n config.maxRedirects < 1\n ) {\n throw new Error(\n 'HTTPClient maxRedirects must be greater than or equal to 1 when followRedirects is true.',\n );\n }\n\n if (adapterType === 'xhr' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with XHR adapter. Set followRedirects: false or use a different adapter/runtime.',\n );\n }\n\n if (adapterType === 'xhr' && !hasXMLHttpRequestGlobal()) {\n throw new Error(\n 'HTTPClient XHR adapter is not supported when XMLHttpRequest is unavailable. Use a browser runtime, install a test shim, or switch to the FetchAdapter/NodeAdapter.',\n );\n }\n\n if (!isBrowserRuntime) {\n return;\n }\n\n if (adapterType === 'node') {\n throw new Error(\n 'HTTPClient Node adapter is not supported in browser environments.',\n );\n }\n\n // MockAdapter is intentionally allowed in browser runtimes: it is an\n // in-memory test adapter, so cookie jars and redirect following are local\n // simulation features rather than forbidden browser networking controls.\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.cookieJar) {\n throw new Error(\n `HTTPClient cookieJar is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers manage cookies automatically.`,\n );\n }\n\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.userAgent) {\n throw new Error(\n `HTTPClient userAgent is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers do not allow overriding the User-Agent header.`,\n );\n }\n\n if (adapterType === 'fetch' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with FetchAdapter in browser environments. Set followRedirects: false or use a server runtime.',\n );\n }\n}\n\nfunction requiresAbsoluteBaseURL(\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): boolean {\n if (adapterType === 'node' || adapterType === 'mock') {\n return true;\n }\n\n if (adapterType === 'fetch' && !isBrowserRuntime) {\n return true;\n }\n\n return false;\n}\n\nfunction hasXMLHttpRequestGlobal(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof (globalThis as { XMLHttpRequest?: unknown }).XMLHttpRequest ===\n 'function'\n );\n}\n\n/**\n * Converts a Headers object (from fetch) into AdapterResponse headers.\n * `set-cookie` is extracted as `string[]` via `getSetCookie()` — the Fetch API\n * would otherwise incorrectly comma-join multiple Set-Cookie values.\n * All other headers are extracted as plain strings.\n */\nexport function extractFetchHeaders(\n headers: Headers,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of headers.entries()) {\n const lower = key.toLowerCase();\n\n if (lower !== 'set-cookie') {\n result[lower] = value;\n }\n }\n\n // Use getSetCookie() when available (Bun, Node 18.14+, modern browsers)\n if (typeof headers.getSetCookie === 'function') {\n const setCookies = headers.getSetCookie();\n\n if (setCookies.length > 0) {\n result['set-cookie'] = setCookies;\n }\n } else {\n // Fallback: headers.get() comma-joins — split on ', ' is unreliable for\n // cookies but better than nothing on older runtimes\n const raw = headers.get('set-cookie');\n\n if (raw) {\n result['set-cookie'] = [raw];\n }\n }\n\n return result;\n}\n\n/**\n * Lowercases all keys on adapter/response header objects. `HTTPClient` runs\n * this on each adapter response before {@link CookieJar.processResponseHeaders}.\n * The jar also normalizes so the same shapes work when feeding headers directly.\n *\n * - Non–`set-cookie` values: if an array appears (unexpected), the first\n * element is kept when read via {@link scalarHeader}.\n * - `set-cookie`: stored as `string[]` — each array entry is one full\n * `Set-Cookie` header line (one cookie). A single string value becomes a\n * one-element array. If the same header appears under keys that differ only\n * by case, those lines are appended in the order they appear on the input\n * object.\n */\nexport function normalizeAdapterResponseHeaders(\n headers: Record<string, string | string[]>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n\n if (lower === 'set-cookie') {\n const chunk = Array.isArray(value) ? value : [value];\n const existing = result[lower];\n\n if (existing === undefined) {\n result[lower] = chunk;\n } else {\n const existingLines = Array.isArray(existing) ? existing : [existing];\n result[lower] = [...existingLines, ...chunk];\n }\n } else {\n result[lower] = Array.isArray(value) ? (value[0] ?? '') : value;\n }\n }\n\n return result;\n}\n\n/**\n * Reads a single-valued header when keys are already lowercase (e.g. after\n * {@link mergeHeaders} on requests or {@link normalizeAdapterResponseHeaders}\n * on responses). If the stored value is `string[]`, returns the first entry.\n */\nexport function scalarHeader(\n headers: Record<string, string | string[]>,\n lowercaseName: string,\n): string | undefined {\n const v = headers[lowercaseName];\n\n if (v === undefined) {\n return undefined;\n }\n\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Resolves a redirect target from response headers when the adapter can\n * observe a redirect response but the client may not follow it itself.\n */\nexport function resolveDetectedRedirectURL(\n requestURL: string,\n status: number,\n headers: Record<string, string | string[]>,\n baseURL?: string,\n): string | undefined {\n if (![301, 302, 303, 307, 308].includes(status)) {\n return undefined;\n }\n\n const location = scalarHeader(headers, 'location');\n\n if (!location) {\n return undefined;\n }\n\n try {\n const absoluteRequestURL = resolveAbsoluteURL(requestURL, baseURL);\n return new URL(location, absoluteRequestURL).toString();\n } catch {\n return location;\n }\n}\n\nexport function assertValidBaseURL(\n baseURL: string,\n fieldName = 'baseURL',\n): void {\n try {\n const url = new URL(baseURL);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('unsupported protocol');\n }\n } catch {\n throw new Error(\n `HTTPClient ${fieldName} must be an absolute http(s) URL (for example \"https://api.example.com\").`,\n );\n }\n}\n\n/**\n * Detects whether the current runtime looks like a browser environment.\n */\nexport function isBrowserEnvironment(): boolean {\n if (typeof globalThis === 'undefined') {\n return false;\n }\n\n if ('window' in globalThis && 'document' in globalThis) {\n return true;\n }\n\n const workerGlobalScope = (\n globalThis as {\n WorkerGlobalScope?: abstract new (...args: never[]) => unknown;\n }\n ).WorkerGlobalScope;\n\n if (\n typeof workerGlobalScope === 'function' &&\n (globalThis as { self?: unknown }).self instanceof workerGlobalScope\n ) {\n return true;\n }\n\n const constructorName = globalThis.constructor?.name;\n\n return (\n !('window' in globalThis) &&\n !('document' in globalThis) &&\n typeof constructorName === 'string' &&\n constructorName.endsWith('WorkerGlobalScope')\n );\n}\n\n/**\n * Serializes the request body and returns the body + inferred content-type.\n * If `formData` is provided, it takes precedence over `body`.\n */\nexport function serializeBody(body: unknown): {\n body: string | Uint8Array | FormData | null;\n contentType: string | null;\n} {\n assertSupportedRequestBody(body);\n\n if (body instanceof FormData) {\n return { body, contentType: null }; // browser/runtime sets multipart boundary automatically\n } else if (body === undefined || body === null) {\n return { body: null, contentType: null };\n } else if (typeof body === 'string') {\n return { body, contentType: 'text/plain; charset=utf-8' };\n } else if (body instanceof Uint8Array) {\n return { body, contentType: 'application/octet-stream' };\n } else if (Array.isArray(body) || isPlainJSONBodyObject(body)) {\n return {\n body: JSON.stringify(body),\n contentType: 'application/json; charset=utf-8',\n };\n } else {\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n }\n}\n\nexport function assertSupportedRequestBody(body: unknown): void {\n if (\n body === undefined ||\n body === null ||\n typeof body === 'string' ||\n body instanceof Uint8Array ||\n body instanceof FormData ||\n Array.isArray(body) ||\n isPlainJSONBodyObject(body)\n ) {\n return;\n }\n\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n}\n\nexport function isPlainJSONBodyObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n\n const prototype = Reflect.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/**\n * Extracts the hostname from a URL string. Returns empty string on failure.\n */\nexport function extractHostname(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/**\n * Wildcard hostname matching backed by {@link matchesWildcardDomain}.\n *\n * - `*` — global wildcard, matches any valid hostname including apex domains\n * - `*.example.com` — matches exactly one subdomain label (`api.example.com`) but not deeper levels or the apex\n * - `**.example.com` — matches one or more subdomain labels (`api.example.com`, `a.b.example.com`) but not the apex\n * - Exact patterns (no `*`) — normalized comparison, case-insensitive\n * - PSL tail guard active: `*.com`, `**.co.uk`, etc. never match\n * - Pseudo-TLD suffix wildcards are rejected just like PSL tails (`*.localhost`, `*.local`, etc.)\n */\nexport function matchesHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.includes('*')) {\n return matchesWildcardDomain(hostname, pattern);\n }\n\n const normalizedHostname = normalizeDomain(hostname);\n const normalizedPattern = normalizeDomain(pattern);\n return normalizedHostname !== '' && normalizedHostname === normalizedPattern;\n}\n\n/**\n * Checks whether a dot-path key exists in a nested object.\n * Arrays are not traversed — only plain objects at each segment.\n */\nfunction hasNestedKey(obj: Record<string, unknown>, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n return false;\n }\n\n if (!(part in (current as Record<string, unknown>))) {\n return false;\n }\n\n current = (current as Record<string, unknown>)[part];\n }\n\n return true;\n}\n\nfunction normalizeMimeType(value: string): string {\n return value.split(';', 1)[0].trim().toLowerCase();\n}\n\nfunction matchesContentTypePattern(\n actualHeader: string,\n pattern: string,\n): boolean {\n const actual = normalizeMimeType(actualHeader);\n const expected = normalizeMimeType(pattern);\n\n if (!actual || !expected) {\n return false;\n }\n\n if (expected.endsWith('/*')) {\n const expectedType = expected.slice(0, -2);\n const slashIndex = actual.indexOf('/');\n\n if (slashIndex === -1) {\n return false;\n }\n\n return actual.slice(0, slashIndex) === expectedType;\n }\n\n return actual === expected;\n}\n\n/**\n * Tests whether a request context matches an interceptor/observer filter.\n *\n * Each filter field is optional — omitting it skips that check entirely.\n * All specified fields must match for the function to return true.\n * Within each field, values are matched with OR logic (any one match is sufficient).\n *\n * - `phases`: **OR** allowlist on `phaseType` ({@link RequestPhaseName}). Skipped when\n * `filter.phases` is omitted or empty.\n * - `statusCodes`: skipped if `context.status` is absent.\n * - `methods`: skipped if `context.method` is absent.\n * - `hosts`: supports exact hostnames and wildcard patterns. `*.example.com` matches\n * exactly one subdomain label; `**.example.com` matches any depth. Neither matches the\n * apex — list it explicitly. PSL tail guard prevents `*.com`-style patterns. `*` is a\n * global wildcard that matches any valid hostname. Skipped if `context.requestURL` is absent.\n * - `schemes`: `'http'` or `'https'`. `requestURL` is absolute whenever the\n * request could be resolved before dispatch. For `MockAdapter`, path-only\n * requests without a client `baseURL` are materialized as `http://localhost/...`;\n * browser adapters fall back to `window.location`, and the Node adapter requires\n * absolute URLs. Skipped only when `requestURL` is absent.\n * - `bodyContainsKeys`: supports dot paths (e.g. `data.results`). Each segment in\n * the path must resolve to a plain object for traversal to continue — the final\n * value can be anything (array, string, null, etc). Array indexing is not supported.\n * Skipped when `kind` is `'error'`.\n */\nexport function matchesFilter(\n filter: {\n statusCodes?: number[];\n methods?: string[];\n bodyContainsKeys?: string[];\n hosts?: string[];\n schemes?: ('http' | 'https')[];\n phases?: RequestPhaseName[];\n contentTypes?: ContentType[];\n contentTypeHeaders?: string[];\n },\n context: {\n status?: number;\n method?: string;\n body?: unknown;\n requestURL?: string;\n contentType?: ContentType;\n contentTypeHeader?: string;\n },\n phaseType: RequestPhaseName,\n kind: 'request' | 'response' | 'error',\n): boolean {\n if (\n filter.phases &&\n filter.phases.length > 0 &&\n !filter.phases.includes(phaseType)\n ) {\n return false;\n }\n\n if (filter.statusCodes && context.status !== undefined) {\n if (!filter.statusCodes.includes(context.status)) {\n return false;\n }\n }\n\n if (filter.methods && context.method) {\n if (!filter.methods.includes(context.method)) {\n return false;\n }\n }\n\n if (filter.contentTypes && filter.contentTypes.length > 0) {\n if (\n !context.contentType ||\n !filter.contentTypes.includes(context.contentType)\n ) {\n return false;\n }\n }\n\n if (filter.contentTypeHeaders && filter.contentTypeHeaders.length > 0) {\n if (\n !context.contentTypeHeader ||\n !filter.contentTypeHeaders.some((pattern) =>\n matchesContentTypePattern(context.contentTypeHeader as string, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (\n kind !== 'error' &&\n filter.bodyContainsKeys &&\n filter.bodyContainsKeys.length > 0\n ) {\n if (\n !context.body ||\n typeof context.body !== 'object' ||\n Array.isArray(context.body)\n ) {\n return false;\n }\n\n const body = context.body as Record<string, unknown>;\n\n if (!filter.bodyContainsKeys.some((k) => hasNestedKey(body, k))) {\n return false;\n }\n }\n\n if (filter.hosts && context.requestURL) {\n const hostname = extractHostname(context.requestURL);\n\n if (\n !filter.hosts.some((pattern: string) =>\n matchesHostPattern(hostname, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (filter.schemes && filter.schemes.length > 0 && context.requestURL) {\n let scheme: 'http' | 'https' | null = null;\n\n try {\n const parsedScheme = new URL(context.requestURL).protocol.replace(\n ':',\n '',\n );\n\n if (parsedScheme === 'http' || parsedScheme === 'https') {\n scheme = parsedScheme;\n }\n } catch {\n scheme = null;\n }\n\n if (!scheme || !filter.schemes.includes(scheme)) {\n return false;\n }\n }\n\n return true;\n}\n","import { XHR_BROWSER_TIMEOUT_FLAG } from '../consts';\nimport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n} from '../types';\nimport { resolveAbsoluteURLForRuntime } from '../utils';\n\n/**\n * XHR-based adapter for environments that expose `XMLHttpRequest`. Primary\n * advantage over FetchAdapter is real per-chunk upload and download progress\n * via `xhr.upload.onprogress` / `xhr.onprogress`. FetchAdapter only fires 0%\n * and 100% because the Fetch API has no streaming upload and requires\n * buffering the full response to read body bytes.\n *\n * XHR constraints compared to FetchAdapter and NodeAdapter:\n * - `followRedirects: false` is required — XHR offers no opt-out from\n * automatic redirect following, so individual hops cannot be observed or\n * controlled. Redirect following is unsupported and treated as an error.\n * - In browser runtimes, cookies, CORS, and restricted headers (e.g. Cookie,\n * User-Agent) are browser-managed; `cookieJar` must not be passed there.\n */\nexport class XHRAdapter implements HTTPAdapter {\n public getType(): AdapterType {\n return 'xhr';\n }\n\n public send(request: AdapterRequest): Promise<AdapterResponse> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n // responseType 'arraybuffer' gives us a raw ArrayBuffer on load,\n // consistent with how FetchAdapter and NodeAdapter deliver body bytes.\n xhr.open(request.method, request.requestURL);\n xhr.responseType = 'arraybuffer';\n\n // Timeout is managed by the client via the abort signal — the client's\n // per-attempt timer fires AbortController.abort(), which propagates to\n // xhr.abort() through the signal listener below. We disable XHR's own\n // timeout mechanism (0 = no timeout) so the client retains full control.\n // The 'timeout' event listener below is kept as a defensive fallback in\n // case a browser fires it anyway (e.g. a hard-coded internal limit).\n xhr.timeout = 0;\n\n // --- Request headers ---\n //\n // Calling setRequestHeader multiple times for the same key causes XHR to\n // combine values with \", \" per spec — which is correct for all headers\n // the browser allows scripts to set. Cookie is a forbidden header name\n // and is silently dropped by the browser regardless; the browser manages\n // cookies on its own.\n for (const [key, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) {\n for (const v of value) {\n xhr.setRequestHeader(key, v);\n }\n } else {\n xhr.setRequestHeader(key, value);\n }\n }\n\n // --- Abort signal ---\n //\n // Check for pre-aborted signal before calling xhr.send — if we called\n // send first and then aborted, the abort event fires asynchronously and\n // we'd resolve the promise rather than reject it with an AbortError.\n if (request.signal) {\n if (request.signal.aborted) {\n reject(new DOMException('Request aborted', 'AbortError'));\n return;\n }\n\n request.signal.addEventListener(\n 'abort',\n () => {\n xhr.abort();\n },\n // once: true — the XHR is already done after the first abort, no\n // need to keep the listener alive and risk a second call.\n { once: true },\n );\n }\n\n // --- Upload progress ---\n\n // Fire initial 0% upload progress before any bytes leave the browser,\n // mirroring the FetchAdapter pattern so callers see a consistent first\n // event regardless of adapter.\n request.onUploadProgress?.({ loaded: 0, total: 0, progress: 0 });\n\n // Real per-chunk upload progress — the main advantage over FetchAdapter,\n // which has no streaming upload and can only fire 0% then 100%.\n // Deduplication guard — upload.progress and upload.load can both report\n // 100% (see upload.load listener below for details).\n let didFireUpload100 = false;\n let uploadedBytes = 0;\n let uploadTotalBytes = 0;\n\n xhr.upload.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireUpload100 = true;\n }\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(uploadTotalBytes, event.total);\n\n request.onUploadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // 100% upload fires as soon as all bytes are sent, always before xhr.load\n // per spec. Skip if upload.progress already reported 100% to avoid a\n // duplicate event. We still track whether this fired so xhr.load can use\n // it as a fallback for environments that skip upload.load entirely.\n let didUploadComplete = false;\n\n xhr.upload.addEventListener('load', (event) => {\n didUploadComplete = true;\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(\n uploadTotalBytes,\n event.total || event.loaded,\n );\n\n if (!didFireUpload100) {\n const finalLoaded = uploadedBytes > 0 ? uploadedBytes : 1;\n const finalTotal = uploadTotalBytes > 0 ? uploadTotalBytes : 1;\n request.onUploadProgress?.({\n loaded: finalLoaded,\n total: finalTotal,\n progress: 1,\n });\n }\n });\n\n // --- Download progress ---\n\n // Real per-chunk download progress. Same advantage over FetchAdapter:\n // FetchAdapter buffers the full response body before firing any progress,\n // so it can only ever report 0% then 100%.\n // Deduplication guard — when Content-Length is known and the final\n // progress chunk reaches 100%, xhr.load would otherwise fire it again.\n let didFireDownload100 = false;\n let downloadedBytes = 0;\n let downloadTotalBytes = 0;\n\n xhr.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireDownload100 = true;\n }\n\n downloadedBytes = Math.max(downloadedBytes, event.loaded);\n downloadTotalBytes = Math.max(downloadTotalBytes, event.total);\n\n request.onDownloadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // --- Load (success) ---\n\n xhr.addEventListener('load', () => {\n // Detect browser-followed redirects.\n //\n // In a browser, FetchAdapter uses `redirect: 'manual'` which yields an\n // opaqueredirect response (status 0) — redirects are intercepted before\n // they happen. XHR has no equivalent opt-out; the browser always follows\n // redirects automatically. We detect them after-the-fact by comparing\n // xhr.responseURL (the final URL after all hops) to the original URL.\n //\n // Both browser adapters surface the same signal: status 0 +\n // wasRedirectDetected, which routes through HTTPClient's\n // redirect_disabled error path so callers get a consistent isFailed\n // response regardless of adapter.\n if (\n xhr.responseURL &&\n didBrowserFollowRedirect(xhr.responseURL, request.requestURL)\n ) {\n // The browser completed the transport and surfaced the final URL even\n // though the client will treat the result as redirect_disabled, so\n // emit terminal progress before returning the synthetic redirect\n // response.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: downloadedBytes,\n total: downloadTotalBytes,\n progress: 1,\n });\n }\n\n resolve({\n status: 0,\n wasRedirectDetected: true,\n // XHR exposes the post-redirect final URL via responseURL. Browser\n // fetch opaque redirects do not, so this is intentionally\n // adapter-specific and surfaced separately from requestURL.\n detectedRedirectURL: xhr.responseURL,\n headers: {},\n body: null,\n });\n return;\n }\n\n // Fallback: upload.load didn't fire (no request body, or the browser\n // skipped the event). Ensure callers always see a 100% upload event,\n // unless upload.progress already reported it.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n const body = readResponseBody(request.method, xhr);\n\n // Final 100% download progress — skip if a progress event already\n // fired exactly 100% (Content-Length known and final chunk completed it).\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: body?.length ?? 0,\n total: body?.length ?? 0,\n progress: 1,\n });\n }\n\n resolve({\n status: xhr.status,\n headers: parseXHRResponseHeaders(xhr.getAllResponseHeaders()),\n body,\n });\n });\n\n // --- Error / timeout / abort ---\n\n // The error event fires for network-level failures (DNS failure, refused\n // connection, CORS rejection). It never fires for HTTP error status codes\n // (4xx, 5xx) — those arrive on the load event with a real status.\n xhr.addEventListener('error', () => {\n resolve({\n status: 0,\n isTransportError: true,\n headers: {},\n body: null,\n errorCause: new Error('XHR network error'),\n });\n });\n\n // Defensive fallback: fires if the browser has a hard-coded internal\n // timeout limit (xhr.timeout is 0 so we never set one ourselves). Mark\n // the error so HTTPClient classifies it as a timeout (retryable) rather\n // than an unexpected abort (non-retryable cancel).\n xhr.addEventListener('timeout', () => {\n reject(\n Object.assign(new DOMException('Request timed out', 'AbortError'), {\n [XHR_BROWSER_TIMEOUT_FLAG]: true,\n }),\n );\n });\n\n xhr.addEventListener('abort', () => {\n reject(new DOMException('Request aborted', 'AbortError'));\n });\n\n xhr.send(prepareBody(request.body));\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the response body as a Uint8Array, or null for response types that\n * carry no body (HEAD, 204 No Content, 304 Not Modified).\n */\nfunction readResponseBody(\n method: string,\n xhr: XMLHttpRequest,\n): Uint8Array | null {\n if (method === 'HEAD' || xhr.status === 204 || xhr.status === 304) {\n return null;\n }\n\n if (xhr.response instanceof ArrayBuffer) {\n return new Uint8Array(xhr.response);\n }\n\n return null;\n}\n\n/**\n * Converts the adapter request body to a value accepted by `xhr.send()`.\n * `string`, `Uint8Array` (BufferSource), and `FormData` are all valid\n * `XMLHttpRequestBodyInit` values — the cast is safe for the body types\n * the client produces.\n */\nfunction prepareBody(\n body: AdapterRequest['body'],\n): XMLHttpRequestBodyInit | null {\n if (body === null) {\n return null;\n }\n\n return body as XMLHttpRequestBodyInit;\n}\n\n/**\n * Parses the raw header string from `xhr.getAllResponseHeaders()` into a\n * lowercase-keyed record.\n *\n * `getAllResponseHeaders()` returns CRLF-delimited `name: value` lines. When\n * a server sends multiple headers with the same name the browser combines them\n * into a single comma-joined line for most headers, but emits each `Set-Cookie`\n * value as its own line (per spec) to avoid ambiguity with the comma in cookie\n * values. Those are collected here as `string[]` to match the\n * `AdapterResponse.headers` contract.\n *\n * Note: browsers unconditionally block `Set-Cookie` and `Set-Cookie2` from\n * `getAllResponseHeaders()` per the XHR spec, so the `set-cookie` array\n * branch below is effectively unreachable in a real browser — it exists to\n * satisfy the shared `AdapterResponse` type contract.\n */\nfunction parseXHRResponseHeaders(\n raw: string,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n if (!raw) {\n return result;\n }\n\n for (const line of raw.split('\\r\\n')) {\n // Lines are `Name: value` pairs separated by the first colon.\n // indexOf is used (not split) so colons in the value are preserved.\n const colonIndex = line.indexOf(':');\n\n if (colonIndex < 0) {\n // No colon — malformed or trailing empty line; skip\n continue;\n }\n\n // Lowercase to normalize across servers (header names are case-insensitive)\n const key = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n\n if (!key) {\n // Colon at position 0 — no name; skip\n continue;\n }\n\n if (key === 'set-cookie') {\n // Each Set-Cookie directive arrives as its own line — collect into an\n // array so callers never need to split on commas (which are valid inside\n // cookie values). Guarded by the XHR spec in standard browsers, but kept\n // for correctness in legacy environments or platforms with non-standard\n // XHR implementations.\n const existing = result['set-cookie'];\n\n if (existing === undefined) {\n result['set-cookie'] = [value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result['set-cookie'] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Compares URLs as browsers evaluate request destinations:\n * - strips hash fragments (not sent over HTTP)\n * - relies on URL normalization for equivalent forms\n * (default ports, dot segments, encoding normalization, etc.)\n */\nfunction didBrowserFollowRedirect(\n responseURL: string,\n requestURL: string,\n): boolean {\n try {\n const normalizedResponse = new URL(responseURL);\n normalizedResponse.hash = '';\n\n const normalizedRequest = new URL(\n resolveAbsoluteURLForRuntime(requestURL, undefined, true),\n normalizedResponse.href,\n );\n normalizedRequest.hash = '';\n\n return normalizedResponse.href !== normalizedRequest.href;\n } catch {\n // Fallback for non-URL inputs: preserve prior behavior.\n return responseURL !== requestURL;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/lib/http-client/consts.ts","../../../src/lib/http-client/utils.ts","../../../src/lib/http-client/adapters/xhr-adapter.ts"],"sourcesContent":["import type { HTTPMethod } from './types';\n\n/**\n * HTTP responses that are plausibly transient and worth retrying when a retry\n * policy is explicitly enabled.\n *\n * `status === 0` is included on purpose because browser/XHR-style adapters can\n * surface \"no real HTTP response\" that way when the network is unavailable or\n * the request otherwise fails before a normal status code is received.\n */\nexport const RETRYABLE_STATUS_CODES: ReadonlySet<number> = new Set([\n // 0: Browser/XHR-style \"no response\" status.\n 0,\n\n // 408 Request Timeout\n 408,\n // 429 Too Many Requests\n 429,\n\n // 500 Internal Server Error\n 500,\n // 502 Bad Gateway\n 502,\n // 503 Service Unavailable\n 503,\n // 504 Gateway Timeout\n 504,\n\n // 507 Insufficient Storage\n 507,\n // 509 Bandwidth Limit Exceeded (non-standard)\n 509,\n // 520 Unknown Error (Cloudflare)\n 520,\n // 521 Web Server Is Down (Cloudflare)\n 521,\n // 522 Connection Timed Out (Cloudflare)\n 522,\n // 523 Origin Is Unreachable (Cloudflare)\n 523,\n // 524 A Timeout Occurred (Cloudflare)\n 524,\n // 598 Network Read Timeout Error (non-standard)\n 598,\n // 599 Network Connect Timeout Error (non-standard)\n 599,\n]);\n\n/**\n * Methods RFC 9110 does not define as idempotent, so replaying one may apply\n * the same change twice.\n *\n * `PUT` and `DELETE` are absent on purpose: both are idempotent by definition,\n * even though they mutate. Repeating them lands the resource in the same state\n * as doing it once, which is exactly what makes a replay safe.\n */\nexport const NON_IDEMPOTENT_METHODS: ReadonlySet<HTTPMethod> =\n new Set<HTTPMethod>(['POST', 'PATCH']);\n\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-local-client-request-id';\n\nexport const DEFAULT_REQUEST_ATTEMPT_HEADER = 'x-local-client-request-attempt';\n\nexport const DEFAULT_USER_AGENT = 'lifecycleion-http-client';\n\nexport const NON_RETRYABLE_HTTP_CLIENT_CALLBACK_ERROR_FLAG =\n '_lifecycleion_non_retryable_http_client_callback_error';\n\nexport const STREAM_FACTORY_ERROR_FLAG = '_lifecycleion_stream_factory_error';\n\n/**\n * Attached to the AbortError thrown when a StreamResponseFactory returns null\n * or `{ cancel: true, reason? }`. The value is the reason string if provided,\n * or `true` if the factory cancelled without a reason. Lets HTTPClient surface\n * the reason on HTTPClientError.cancelReason.\n */\nexport const STREAM_FACTORY_CANCEL_KEY =\n '_lifecycleion_stream_factory_cancel_reason';\n\nexport const RESPONSE_STREAM_ABORT_FLAG = '_lifecycleion_response_stream_abort';\n\n/**\n * Set on the AbortError thrown by XHRAdapter's defensive `timeout` event\n * listener. Lets HTTPClient classify the error as a timeout (retryable) rather\n * than an unexpected abort (non-retryable cancel).\n */\nexport const XHR_BROWSER_TIMEOUT_FLAG = '_lifecycleion_xhr_browser_timeout';\n\nexport const HTTP_METHODS: ReadonlyArray<HTTPMethod> = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n];\n\n/**\n * Exact-match request headers that browsers either forbid outright or do not\n * let this client set reliably via plain Fetch/XHR headers.\n *\n * Prefix-based rules like `proxy-*` and `sec-*` are handled in `header-utils.ts`.\n */\nexport const BROWSER_RESTRICTED_HEADERS: ReadonlySet<string> = new Set([\n // Encoding / CORS negotiation headers controlled by the browser.\n 'accept-charset',\n 'accept-encoding',\n 'access-control-request-headers',\n 'access-control-request-method',\n 'access-control-request-private-network',\n\n // Connection-level transport headers.\n 'connection',\n 'content-length',\n 'date',\n 'expect',\n 'host',\n 'keep-alive',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'via',\n\n // Browser-managed request context / privacy headers.\n 'cookie',\n 'dnt',\n 'origin',\n 'referer',\n 'set-cookie',\n 'user-agent',\n]);\n\nexport const BROWSER_RESTRICTED_HEADER_PREFIXES: ReadonlyArray<string> = [\n 'proxy-',\n 'sec-',\n];\n\n/**\n * Headers that can tunnel the real method through POST. Browsers block these\n * when they try to smuggle forbidden transport methods.\n */\nexport const BROWSER_METHOD_OVERRIDE_HEADER_NAMES: ReadonlySet<string> =\n new Set(['x-http-method', 'x-http-method-override', 'x-method-override']);\n\n/**\n * Methods that browsers do not allow request headers to tunnel via the\n * override headers above.\n */\nexport const BROWSER_FORBIDDEN_METHOD_OVERRIDE_VALUES: ReadonlySet<string> =\n new Set(['connect', 'trace', 'track']);\n\nexport const DEFAULT_MAX_REDIRECTS = 5;\n\n/**\n * Redirect responses that carry a follow-up `Location` hop. `300` and `304`\n * are excluded because they do not represent an automatic redirect here.\n */\nexport const REDIRECT_STATUS_CODES: ReadonlySet<number> = new Set([\n // 301 Moved Permanently\n 301,\n // 302 Found\n 302,\n\n // 303 See Other\n 303,\n\n // 307 Temporary Redirect\n 307,\n // 308 Permanent Redirect\n 308,\n]);\n","import qs from 'qs';\nimport {\n matchesWildcardDomain,\n normalizeDomain,\n} from '../domain-utils/domain-utils';\nimport type {\n ContentType,\n RequestPhaseName,\n HTTPClientConfig,\n AdapterType,\n} from './types';\n\n/**\n * If `path` is an absolute HTTP(S) URL, returns its canonical `href` (normalized\n * scheme/host casing). Otherwise `null`. Protocol-relative `//host` is handled\n * separately in `buildURL`.\n *\n * Rules differ from `resolveAbsoluteURL` (which accepts any absolute scheme).\n * One parse here per request is negligible next to network I/O.\n */\nfunction tryAbsoluteWebHref(path: string): string | null {\n if (!path || path.startsWith('//')) {\n return null;\n }\n\n try {\n const u = new URL(path);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n return u.href;\n }\n } catch {\n // not parseable as absolute\n }\n\n return null;\n}\n\n/**\n * Builds a request URL string from `baseURL`, `path`, and optional query params\n * (`qs` — nested objects and arrays supported).\n *\n * **Relative paths (usual case)** — When `baseURL` is set and `path` is not\n * absolute, `path` is joined to `baseURL` (leading slash normalized). Example:\n * `baseURL: https://api.test`, `path: /v1/users` → `https://api.test/v1/users`.\n *\n * **Absolute / protocol-relative `path` (escape hatch)** — If `path` is a full\n * `http:` or `https:` URL, it is **not** prefixed with `baseURL` (after\n * normalization via `URL#href`). The same applies to protocol-relative URLs\n * (`//cdn.example/x`): they are left for {@link resolveAbsoluteURL} to resolve\n * using the client `baseURL`’s scheme. Use this for one-off cross-origin calls,\n * CDN assets, or URLs returned by APIs; for strict per-origin clients, prefer\n * relative paths and a dedicated client or `HTTPClient.createSubClient()` per\n * origin.\n */\nexport function buildURL(\n baseURL: string | undefined,\n path: string,\n params?: Record<string, unknown>,\n): string {\n let url: string;\n\n const absoluteHref = tryAbsoluteWebHref(path);\n\n if (baseURL && absoluteHref === null && !path.startsWith('//')) {\n // Avoid double slashes when joining base + path\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const p = path.startsWith('/') ? path : `/${path}`;\n url = `${base}${p}`;\n } else if (absoluteHref !== null) {\n url = absoluteHref;\n } else {\n url = path;\n }\n\n if (params && Object.keys(params).length > 0) {\n const [urlWithoutHash, hash = ''] = url.split('#', 2);\n const queryStartIndex = urlWithoutHash.indexOf('?');\n\n if (queryStartIndex === -1) {\n const queryString = qs.stringify(params, { addQueryPrefix: true });\n url = `${urlWithoutHash}${queryString}${hash ? `#${hash}` : ''}`;\n } else {\n const basePath = urlWithoutHash.slice(0, queryStartIndex);\n const existingQuery = urlWithoutHash.slice(queryStartIndex + 1);\n // Fragments are preserved only as part of the caller's URL string.\n // They are not transmitted in HTTP requests, but keeping them intact\n // makes buildURL safer as a general-purpose URL composition helper.\n const mergedParams = {\n ...qs.parse(existingQuery),\n ...params,\n };\n\n const queryString = qs.stringify(mergedParams, { addQueryPrefix: true });\n url = `${basePath}${queryString}${hash ? `#${hash}` : ''}`;\n }\n }\n\n return url;\n}\n\n/**\n * Best-effort absolute URL for logging, redirects, and hop metadata.\n *\n * - If `url` parses as an absolute URL (has a scheme), returns normalized `href`.\n * - Otherwise, when `baseURL` is set, resolves `url` against it (path-relative,\n * same-host relative, protocol-relative `//host`, query-only, etc.).\n * - If neither works, returns `url` unchanged (callers without `baseURL` may still\n * see path-only strings).\n */\nexport function resolveAbsoluteURL(url: string, baseURL?: string): string {\n if (!url) {\n return url;\n }\n\n try {\n return new URL(url).href;\n } catch {\n // Not a standalone absolute URL\n }\n\n if (baseURL) {\n try {\n const base = baseURL.endsWith('/') ? baseURL : `${baseURL}/`;\n return new URL(url, base).href;\n } catch {\n // fall through\n }\n }\n\n return url;\n}\n\n/**\n * Browser-aware absolute URL resolution used by HTTPClient before interceptors\n * and adapter dispatch. Starts with normal baseURL resolution, then falls back\n * to the current page/worker location when running in a browser-like runtime.\n */\nexport function resolveAbsoluteURLForRuntime(\n url: string,\n baseURL: string | undefined,\n isBrowserRuntime: boolean,\n): string {\n const resolved = resolveAbsoluteURL(url, baseURL);\n\n if (\n !isBrowserRuntime ||\n resolved.startsWith('http://') ||\n resolved.startsWith('https://')\n ) {\n return resolved;\n }\n\n const browserBase = getBrowserResolutionBase();\n\n if (!browserBase) {\n return resolved;\n }\n\n return resolveAbsoluteURL(resolved, browserBase);\n}\n\nfunction getBrowserResolutionBase(): string | undefined {\n if (\n typeof document !== 'undefined' &&\n typeof document.baseURI === 'string' &&\n document.baseURI\n ) {\n return document.baseURI;\n }\n\n if (\n typeof window !== 'undefined' &&\n window.location &&\n typeof window.location.href === 'string' &&\n window.location.href\n ) {\n return window.location.href;\n }\n\n const globalLocation = (globalThis as { location?: { href?: unknown } })\n .location;\n\n if (\n globalLocation &&\n typeof globalLocation.href === 'string' &&\n globalLocation.href\n ) {\n return globalLocation.href;\n }\n\n const selfLocation = (\n globalThis as { self?: { location?: { href?: unknown } } }\n ).self?.location;\n\n if (\n selfLocation &&\n typeof selfLocation.href === 'string' &&\n selfLocation.href\n ) {\n return selfLocation.href;\n }\n\n return undefined;\n}\n\n/**\n * Normalizes header keys to lowercase.\n */\nexport function normalizeHeaders(\n headers: Record<string, string>,\n): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = value;\n }\n\n return result;\n}\n\n/**\n * Merges multiple request-header objects, normalizing keys to lowercase.\n * Later objects win on conflict. Array values replace earlier scalars/arrays\n * wholesale, and single-item arrays are collapsed back to a plain string.\n */\nexport function mergeHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? normalizeMergedHeaderArray(value)\n : String(value);\n }\n }\n\n return result;\n}\n\nfunction normalizeMergedHeaderArray(value: string[]): string | string[] {\n const normalized = value.map((item) => String(item));\n return normalized.length === 1 ? normalized[0] : normalized;\n}\n\nexport function mergeObservedHeaders(\n ...headerSets: Array<Record<string, string | string[]> | undefined>\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const headers of headerSets) {\n if (!headers) {\n continue;\n }\n\n for (const [key, value] of Object.entries(headers)) {\n result[key.toLowerCase()] = Array.isArray(value)\n ? value.map((item) => String(item))\n : String(value);\n }\n }\n\n return result;\n}\n\n/**\n * Parses the Content-Type header into a ContentType enum value.\n */\nexport function parseContentType(\n contentTypeHeader: string | undefined,\n): ContentType {\n if (!contentTypeHeader) {\n return 'binary';\n } else {\n const lower = contentTypeHeader.trim().toLowerCase();\n\n if (lower.includes('application/json') || lower.includes('+json')) {\n return 'json';\n } else if (lower.startsWith('text/')) {\n return 'text';\n } else if (lower.includes('application/x-www-form-urlencoded')) {\n return 'text';\n } else {\n return 'binary';\n }\n }\n}\n\n/**\n * Validates adapter/runtime combinations and redirect config before a client\n * is constructed, so unsupported browser-only/server-only options fail fast\n * with clear errors instead of surfacing later during request dispatch.\n */\nexport function assertSupportedAdapterRuntimeAndConfig(\n config: HTTPClientConfig,\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): void {\n if (\n config.baseURL !== undefined &&\n requiresAbsoluteBaseURL(adapterType, isBrowserRuntime)\n ) {\n assertValidBaseURL(config.baseURL);\n }\n\n if (config.maxRedirects !== undefined && config.followRedirects !== true) {\n throw new Error('HTTPClient maxRedirects requires followRedirects: true.');\n }\n\n if (\n config.followRedirects === true &&\n config.maxRedirects !== undefined &&\n config.maxRedirects < 1\n ) {\n throw new Error(\n 'HTTPClient maxRedirects must be greater than or equal to 1 when followRedirects is true.',\n );\n }\n\n if (adapterType === 'xhr' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with XHR adapter. Set followRedirects: false or use a different adapter/runtime.',\n );\n }\n\n if (adapterType === 'xhr' && !hasXMLHttpRequestGlobal()) {\n throw new Error(\n 'HTTPClient XHR adapter is not supported when XMLHttpRequest is unavailable. Use a browser runtime, install a test shim, or switch to the FetchAdapter/NodeAdapter.',\n );\n }\n\n if (!isBrowserRuntime) {\n return;\n }\n\n if (adapterType === 'node') {\n throw new Error(\n 'HTTPClient Node adapter is not supported in browser environments.',\n );\n }\n\n // MockAdapter is intentionally allowed in browser runtimes: it is an\n // in-memory test adapter, so cookie jars and redirect following are local\n // simulation features rather than forbidden browser networking controls.\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.cookieJar) {\n throw new Error(\n `HTTPClient cookieJar is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers manage cookies automatically.`,\n );\n }\n\n if ((adapterType === 'fetch' || adapterType === 'xhr') && config.userAgent) {\n throw new Error(\n `HTTPClient userAgent is not supported with ${adapterType === 'fetch' ? 'FetchAdapter' : 'XHR adapter'} in browser environments. Browsers do not allow overriding the User-Agent header.`,\n );\n }\n\n if (adapterType === 'fetch' && config.followRedirects === true) {\n throw new Error(\n 'HTTPClient redirect handling is not supported with FetchAdapter in browser environments. Set followRedirects: false or use a server runtime.',\n );\n }\n}\n\nfunction requiresAbsoluteBaseURL(\n adapterType: AdapterType,\n isBrowserRuntime: boolean,\n): boolean {\n if (adapterType === 'node' || adapterType === 'mock') {\n return true;\n }\n\n if (adapterType === 'fetch' && !isBrowserRuntime) {\n return true;\n }\n\n return false;\n}\n\nfunction hasXMLHttpRequestGlobal(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof (globalThis as { XMLHttpRequest?: unknown }).XMLHttpRequest ===\n 'function'\n );\n}\n\n/**\n * Converts a Headers object (from fetch) into AdapterResponse headers.\n * `set-cookie` is extracted as `string[]` via `getSetCookie()` — the Fetch API\n * would otherwise incorrectly comma-join multiple Set-Cookie values.\n * All other headers are extracted as plain strings.\n */\nexport function extractFetchHeaders(\n headers: Headers,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of headers.entries()) {\n const lower = key.toLowerCase();\n\n if (lower !== 'set-cookie') {\n result[lower] = value;\n }\n }\n\n // Use getSetCookie() when available (Bun, Node 18.14+, modern browsers)\n if (typeof headers.getSetCookie === 'function') {\n const setCookies = headers.getSetCookie();\n\n if (setCookies.length > 0) {\n result['set-cookie'] = setCookies;\n }\n } else {\n // Fallback: headers.get() comma-joins — split on ', ' is unreliable for\n // cookies but better than nothing on older runtimes\n const raw = headers.get('set-cookie');\n\n if (raw) {\n result['set-cookie'] = [raw];\n }\n }\n\n return result;\n}\n\n/**\n * Lowercases all keys on adapter/response header objects. `HTTPClient` runs\n * this on each adapter response before {@link CookieJar.processResponseHeaders}.\n * The jar also normalizes so the same shapes work when feeding headers directly.\n *\n * - Non–`set-cookie` values: if an array appears (unexpected), the first\n * element is kept when read via {@link scalarHeader}.\n * - `set-cookie`: stored as `string[]` — each array entry is one full\n * `Set-Cookie` header line (one cookie). A single string value becomes a\n * one-element array. If the same header appears under keys that differ only\n * by case, those lines are appended in the order they appear on the input\n * object.\n */\nexport function normalizeAdapterResponseHeaders(\n headers: Record<string, string | string[]>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n\n if (lower === 'set-cookie') {\n const chunk = Array.isArray(value) ? value : [value];\n const existing = result[lower];\n\n if (existing === undefined) {\n result[lower] = chunk;\n } else {\n const existingLines = Array.isArray(existing) ? existing : [existing];\n result[lower] = [...existingLines, ...chunk];\n }\n } else {\n result[lower] = Array.isArray(value) ? (value[0] ?? '') : value;\n }\n }\n\n return result;\n}\n\n/**\n * Reads a single-valued header when keys are already lowercase (e.g. after\n * {@link mergeHeaders} on requests or {@link normalizeAdapterResponseHeaders}\n * on responses). If the stored value is `string[]`, returns the first entry.\n */\nexport function scalarHeader(\n headers: Record<string, string | string[]>,\n lowercaseName: string,\n): string | undefined {\n const v = headers[lowercaseName];\n\n if (v === undefined) {\n return undefined;\n }\n\n return Array.isArray(v) ? v[0] : v;\n}\n\n/**\n * Resolves a redirect target from response headers when the adapter can\n * observe a redirect response but the client may not follow it itself.\n */\nexport function resolveDetectedRedirectURL(\n requestURL: string,\n status: number,\n headers: Record<string, string | string[]>,\n baseURL?: string,\n): string | undefined {\n if (![301, 302, 303, 307, 308].includes(status)) {\n return undefined;\n }\n\n const location = scalarHeader(headers, 'location');\n\n if (!location) {\n return undefined;\n }\n\n try {\n const absoluteRequestURL = resolveAbsoluteURL(requestURL, baseURL);\n return new URL(location, absoluteRequestURL).toString();\n } catch {\n return location;\n }\n}\n\nexport function assertValidBaseURL(\n baseURL: string,\n fieldName = 'baseURL',\n): void {\n try {\n const url = new URL(baseURL);\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('unsupported protocol');\n }\n } catch {\n throw new Error(\n `HTTPClient ${fieldName} must be an absolute http(s) URL (for example \"https://api.example.com\").`,\n );\n }\n}\n\n/**\n * Detects whether the current runtime looks like a browser environment.\n */\nexport function isBrowserEnvironment(): boolean {\n if (typeof globalThis === 'undefined') {\n return false;\n }\n\n if ('window' in globalThis && 'document' in globalThis) {\n return true;\n }\n\n const workerGlobalScope = (\n globalThis as {\n WorkerGlobalScope?: abstract new (...args: never[]) => unknown;\n }\n ).WorkerGlobalScope;\n\n if (\n typeof workerGlobalScope === 'function' &&\n (globalThis as { self?: unknown }).self instanceof workerGlobalScope\n ) {\n return true;\n }\n\n const constructorName = globalThis.constructor?.name;\n\n return (\n !('window' in globalThis) &&\n !('document' in globalThis) &&\n typeof constructorName === 'string' &&\n constructorName.endsWith('WorkerGlobalScope')\n );\n}\n\n/**\n * Serializes the request body and returns the body + inferred content-type.\n * If `formData` is provided, it takes precedence over `body`.\n */\nexport function serializeBody(body: unknown): {\n body: string | Uint8Array | FormData | null;\n contentType: string | null;\n} {\n assertSupportedRequestBody(body);\n\n if (body instanceof FormData) {\n return { body, contentType: null }; // browser/runtime sets multipart boundary automatically\n } else if (body === undefined || body === null) {\n return { body: null, contentType: null };\n } else if (typeof body === 'string') {\n return { body, contentType: 'text/plain; charset=utf-8' };\n } else if (body instanceof Uint8Array) {\n return { body, contentType: 'application/octet-stream' };\n } else if (Array.isArray(body) || isPlainJSONBodyObject(body)) {\n return {\n body: JSON.stringify(body),\n contentType: 'application/json; charset=utf-8',\n };\n } else {\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n }\n}\n\nexport function assertSupportedRequestBody(body: unknown): void {\n if (\n body === undefined ||\n body === null ||\n typeof body === 'string' ||\n body instanceof Uint8Array ||\n body instanceof FormData ||\n Array.isArray(body) ||\n isPlainJSONBodyObject(body)\n ) {\n return;\n }\n\n throw new Error(\n 'Unsupported request body type. Supported types: string, Uint8Array, FormData, plain object, array, null, and undefined.',\n );\n}\n\nexport function isPlainJSONBodyObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n\n const prototype = Reflect.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/**\n * Extracts the hostname from a URL string. Returns empty string on failure.\n */\nexport function extractHostname(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/**\n * Wildcard hostname matching backed by {@link matchesWildcardDomain}.\n *\n * - `*` — global wildcard, matches any valid hostname including apex domains\n * - `*.example.com` — matches exactly one subdomain label (`api.example.com`) but not deeper levels or the apex\n * - `**.example.com` — matches one or more subdomain labels (`api.example.com`, `a.b.example.com`) but not the apex\n * - Exact patterns (no `*`) — normalized comparison, case-insensitive\n * - PSL tail guard active: `*.com`, `**.co.uk`, etc. never match\n * - Pseudo-TLD suffix wildcards are rejected just like PSL tails (`*.localhost`, `*.local`, etc.)\n */\nexport function matchesHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.includes('*')) {\n return matchesWildcardDomain(hostname, pattern);\n }\n\n const normalizedHostname = normalizeDomain(hostname);\n const normalizedPattern = normalizeDomain(pattern);\n return normalizedHostname !== '' && normalizedHostname === normalizedPattern;\n}\n\n/**\n * Checks whether a dot-path key exists in a nested object.\n * Arrays are not traversed — only plain objects at each segment.\n */\nfunction hasNestedKey(obj: Record<string, unknown>, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n return false;\n }\n\n if (!(part in (current as Record<string, unknown>))) {\n return false;\n }\n\n current = (current as Record<string, unknown>)[part];\n }\n\n return true;\n}\n\nfunction normalizeMimeType(value: string): string {\n return value.split(';', 1)[0].trim().toLowerCase();\n}\n\nfunction matchesContentTypePattern(\n actualHeader: string,\n pattern: string,\n): boolean {\n const actual = normalizeMimeType(actualHeader);\n const expected = normalizeMimeType(pattern);\n\n if (!actual || !expected) {\n return false;\n }\n\n if (expected.endsWith('/*')) {\n const expectedType = expected.slice(0, -2);\n const slashIndex = actual.indexOf('/');\n\n if (slashIndex === -1) {\n return false;\n }\n\n return actual.slice(0, slashIndex) === expectedType;\n }\n\n return actual === expected;\n}\n\n/**\n * Tests whether a request context matches an interceptor/observer filter.\n *\n * Each filter field is optional — omitting it skips that check entirely.\n * All specified fields must match for the function to return true.\n * Within each field, values are matched with OR logic (any one match is sufficient).\n *\n * - `phases`: **OR** allowlist on `phaseType` ({@link RequestPhaseName}). Skipped when\n * `filter.phases` is omitted or empty.\n * - `statusCodes`: skipped if `context.status` is absent.\n * - `methods`: skipped if `context.method` is absent.\n * - `hosts`: supports exact hostnames and wildcard patterns. `*.example.com` matches\n * exactly one subdomain label; `**.example.com` matches any depth. Neither matches the\n * apex — list it explicitly. PSL tail guard prevents `*.com`-style patterns. `*` is a\n * global wildcard that matches any valid hostname. Skipped if `context.requestURL` is absent.\n * - `schemes`: `'http'` or `'https'`. `requestURL` is absolute whenever the\n * request could be resolved before dispatch. For `MockAdapter`, path-only\n * requests without a client `baseURL` are materialized as `http://localhost/...`;\n * browser adapters fall back to `window.location`, and the Node adapter requires\n * absolute URLs. Skipped only when `requestURL` is absent.\n * - `bodyContainsKeys`: supports dot paths (e.g. `data.results`). Each segment in\n * the path must resolve to a plain object for traversal to continue — the final\n * value can be anything (array, string, null, etc). Array indexing is not supported.\n * Skipped when `kind` is `'error'`.\n */\nexport function matchesFilter(\n filter: {\n statusCodes?: number[];\n methods?: string[];\n bodyContainsKeys?: string[];\n hosts?: string[];\n schemes?: ('http' | 'https')[];\n phases?: RequestPhaseName[];\n contentTypes?: ContentType[];\n contentTypeHeaders?: string[];\n },\n context: {\n status?: number;\n method?: string;\n body?: unknown;\n requestURL?: string;\n contentType?: ContentType;\n contentTypeHeader?: string;\n },\n phaseType: RequestPhaseName,\n kind: 'request' | 'response' | 'error',\n): boolean {\n if (\n filter.phases &&\n filter.phases.length > 0 &&\n !filter.phases.includes(phaseType)\n ) {\n return false;\n }\n\n if (filter.statusCodes && context.status !== undefined) {\n if (!filter.statusCodes.includes(context.status)) {\n return false;\n }\n }\n\n if (filter.methods && context.method) {\n if (!filter.methods.includes(context.method)) {\n return false;\n }\n }\n\n if (filter.contentTypes && filter.contentTypes.length > 0) {\n if (\n !context.contentType ||\n !filter.contentTypes.includes(context.contentType)\n ) {\n return false;\n }\n }\n\n if (filter.contentTypeHeaders && filter.contentTypeHeaders.length > 0) {\n if (\n !context.contentTypeHeader ||\n !filter.contentTypeHeaders.some((pattern) =>\n matchesContentTypePattern(context.contentTypeHeader as string, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (\n kind !== 'error' &&\n filter.bodyContainsKeys &&\n filter.bodyContainsKeys.length > 0\n ) {\n if (\n !context.body ||\n typeof context.body !== 'object' ||\n Array.isArray(context.body)\n ) {\n return false;\n }\n\n const body = context.body as Record<string, unknown>;\n\n if (!filter.bodyContainsKeys.some((k) => hasNestedKey(body, k))) {\n return false;\n }\n }\n\n if (filter.hosts && context.requestURL) {\n const hostname = extractHostname(context.requestURL);\n\n if (\n !filter.hosts.some((pattern: string) =>\n matchesHostPattern(hostname, pattern),\n )\n ) {\n return false;\n }\n }\n\n if (filter.schemes && filter.schemes.length > 0 && context.requestURL) {\n let scheme: 'http' | 'https' | null = null;\n\n try {\n const parsedScheme = new URL(context.requestURL).protocol.replace(\n ':',\n '',\n );\n\n if (parsedScheme === 'http' || parsedScheme === 'https') {\n scheme = parsedScheme;\n }\n } catch {\n scheme = null;\n }\n\n if (!scheme || !filter.schemes.includes(scheme)) {\n return false;\n }\n }\n\n return true;\n}\n","import { XHR_BROWSER_TIMEOUT_FLAG } from '../consts';\nimport type {\n HTTPAdapter,\n AdapterRequest,\n AdapterResponse,\n AdapterType,\n} from '../types';\nimport { resolveAbsoluteURLForRuntime } from '../utils';\n\n/**\n * XHR-based adapter for environments that expose `XMLHttpRequest`. Primary\n * advantage over FetchAdapter is real per-chunk upload and download progress\n * via `xhr.upload.onprogress` / `xhr.onprogress`. FetchAdapter only fires 0%\n * and 100% because the Fetch API has no streaming upload and requires\n * buffering the full response to read body bytes.\n *\n * XHR constraints compared to FetchAdapter and NodeAdapter:\n * - `followRedirects: false` is required — XHR offers no opt-out from\n * automatic redirect following, so individual hops cannot be observed or\n * controlled. Redirect following is unsupported and treated as an error.\n * - In browser runtimes, cookies, CORS, and restricted headers (e.g. Cookie,\n * User-Agent) are browser-managed; `cookieJar` must not be passed there.\n */\nexport class XHRAdapter implements HTTPAdapter {\n public getType(): AdapterType {\n return 'xhr';\n }\n\n public send(request: AdapterRequest): Promise<AdapterResponse> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n // responseType 'arraybuffer' gives us a raw ArrayBuffer on load,\n // consistent with how FetchAdapter and NodeAdapter deliver body bytes.\n xhr.open(request.method, request.requestURL);\n xhr.responseType = 'arraybuffer';\n\n // Timeout is managed by the client via the abort signal — the client's\n // per-attempt timer fires AbortController.abort(), which propagates to\n // xhr.abort() through the signal listener below. We disable XHR's own\n // timeout mechanism (0 = no timeout) so the client retains full control.\n // The 'timeout' event listener below is kept as a defensive fallback in\n // case a browser fires it anyway (e.g. a hard-coded internal limit).\n xhr.timeout = 0;\n\n // --- Request headers ---\n //\n // Calling setRequestHeader multiple times for the same key causes XHR to\n // combine values with \", \" per spec — which is correct for all headers\n // the browser allows scripts to set. Cookie is a forbidden header name\n // and is silently dropped by the browser regardless; the browser manages\n // cookies on its own.\n for (const [key, value] of Object.entries(request.headers)) {\n if (Array.isArray(value)) {\n for (const v of value) {\n xhr.setRequestHeader(key, v);\n }\n } else {\n xhr.setRequestHeader(key, value);\n }\n }\n\n // --- Abort signal ---\n //\n // Check for pre-aborted signal before calling xhr.send — if we called\n // send first and then aborted, the abort event fires asynchronously and\n // we'd resolve the promise rather than reject it with an AbortError.\n if (request.signal) {\n if (request.signal.aborted) {\n reject(new DOMException('Request aborted', 'AbortError'));\n return;\n }\n\n request.signal.addEventListener(\n 'abort',\n () => {\n xhr.abort();\n },\n // once: true — the XHR is already done after the first abort, no\n // need to keep the listener alive and risk a second call.\n { once: true },\n );\n }\n\n // --- Upload progress ---\n\n // Fire initial 0% upload progress before any bytes leave the browser,\n // mirroring the FetchAdapter pattern so callers see a consistent first\n // event regardless of adapter.\n request.onUploadProgress?.({ loaded: 0, total: 0, progress: 0 });\n\n // Real per-chunk upload progress — the main advantage over FetchAdapter,\n // which has no streaming upload and can only fire 0% then 100%.\n // Deduplication guard — upload.progress and upload.load can both report\n // 100% (see upload.load listener below for details).\n let didFireUpload100 = false;\n let uploadedBytes = 0;\n let uploadTotalBytes = 0;\n\n xhr.upload.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireUpload100 = true;\n }\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(uploadTotalBytes, event.total);\n\n request.onUploadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // 100% upload fires as soon as all bytes are sent, always before xhr.load\n // per spec. Skip if upload.progress already reported 100% to avoid a\n // duplicate event. We still track whether this fired so xhr.load can use\n // it as a fallback for environments that skip upload.load entirely.\n let didUploadComplete = false;\n\n xhr.upload.addEventListener('load', (event) => {\n didUploadComplete = true;\n\n uploadedBytes = Math.max(uploadedBytes, event.loaded);\n uploadTotalBytes = Math.max(\n uploadTotalBytes,\n event.total || event.loaded,\n );\n\n if (!didFireUpload100) {\n const finalLoaded = uploadedBytes > 0 ? uploadedBytes : 1;\n const finalTotal = uploadTotalBytes > 0 ? uploadTotalBytes : 1;\n request.onUploadProgress?.({\n loaded: finalLoaded,\n total: finalTotal,\n progress: 1,\n });\n }\n });\n\n // --- Download progress ---\n\n // Real per-chunk download progress. Same advantage over FetchAdapter:\n // FetchAdapter buffers the full response body before firing any progress,\n // so it can only ever report 0% then 100%.\n // Deduplication guard — when Content-Length is known and the final\n // progress chunk reaches 100%, xhr.load would otherwise fire it again.\n let didFireDownload100 = false;\n let downloadedBytes = 0;\n let downloadTotalBytes = 0;\n\n xhr.addEventListener('progress', (event) => {\n const progress = event.lengthComputable\n ? event.loaded / event.total\n : -1;\n\n if (progress === 1) {\n didFireDownload100 = true;\n }\n\n downloadedBytes = Math.max(downloadedBytes, event.loaded);\n downloadTotalBytes = Math.max(downloadTotalBytes, event.total);\n\n request.onDownloadProgress?.({\n loaded: event.loaded,\n total: event.total || 0,\n progress,\n });\n });\n\n // --- Load (success) ---\n\n xhr.addEventListener('load', () => {\n // Detect browser-followed redirects.\n //\n // In a browser, FetchAdapter uses `redirect: 'manual'` which yields an\n // opaqueredirect response (status 0) — redirects are intercepted before\n // they happen. XHR has no equivalent opt-out; the browser always follows\n // redirects automatically. We detect them after-the-fact by comparing\n // xhr.responseURL (the final URL after all hops) to the original URL.\n //\n // Both browser adapters surface the same signal: status 0 +\n // wasRedirectDetected, which routes through HTTPClient's\n // redirect_disabled error path so callers get a consistent isFailed\n // response regardless of adapter.\n if (\n xhr.responseURL &&\n didBrowserFollowRedirect(xhr.responseURL, request.requestURL)\n ) {\n // The browser completed the transport and surfaced the final URL even\n // though the client will treat the result as redirect_disabled, so\n // emit terminal progress before returning the synthetic redirect\n // response.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: downloadedBytes,\n total: downloadTotalBytes,\n progress: 1,\n });\n }\n\n resolve({\n status: 0,\n wasRedirectDetected: true,\n // XHR exposes the post-redirect final URL via responseURL. Browser\n // fetch opaque redirects do not, so this is intentionally\n // adapter-specific and surfaced separately from requestURL.\n detectedRedirectURL: xhr.responseURL,\n headers: {},\n body: null,\n });\n return;\n }\n\n // Fallback: upload.load didn't fire (no request body, or the browser\n // skipped the event). Ensure callers always see a 100% upload event,\n // unless upload.progress already reported it.\n if (!didUploadComplete && !didFireUpload100) {\n request.onUploadProgress?.({\n loaded: uploadedBytes,\n total: uploadTotalBytes,\n progress: 1,\n });\n }\n\n const body = readResponseBody(request.method, xhr);\n\n // Final 100% download progress — skip if a progress event already\n // fired exactly 100% (Content-Length known and final chunk completed it).\n if (!didFireDownload100) {\n request.onDownloadProgress?.({\n loaded: body?.length ?? 0,\n total: body?.length ?? 0,\n progress: 1,\n });\n }\n\n resolve({\n status: xhr.status,\n headers: parseXHRResponseHeaders(xhr.getAllResponseHeaders()),\n body,\n });\n });\n\n // --- Error / timeout / abort ---\n\n // The error event fires for network-level failures (DNS failure, refused\n // connection, CORS rejection). It never fires for HTTP error status codes\n // (4xx, 5xx) — those arrive on the load event with a real status.\n xhr.addEventListener('error', () => {\n // Neither replay signal is set here, on purpose. This adapter can never\n // prove non-delivery: upload progress is suppressed for cross-origin\n // requests CORS does not grant access to, and those are still delivered —\n // the browser blocks the response, not the request. The omission is what\n // keeps a `POST` from being replayed by default.\n //\n // `isRetryable: false` does not stand in for it: that flag blocks every\n // method, so it would stop retrying an idempotent `PUT` after an ordinary\n // network error and override `retryNonIdempotentMethods`.\n resolve({\n status: 0,\n isTransportError: true,\n headers: {},\n body: null,\n errorCause: new Error('XHR network error'),\n });\n });\n\n // Defensive fallback: fires if the browser has a hard-coded internal\n // timeout limit (xhr.timeout is 0 so we never set one ourselves). Mark\n // the error so HTTPClient classifies it as a timeout (retryable) rather\n // than an unexpected abort (non-retryable cancel).\n xhr.addEventListener('timeout', () => {\n reject(\n Object.assign(new DOMException('Request timed out', 'AbortError'), {\n [XHR_BROWSER_TIMEOUT_FLAG]: true,\n }),\n );\n });\n\n xhr.addEventListener('abort', () => {\n reject(new DOMException('Request aborted', 'AbortError'));\n });\n\n xhr.send(prepareBody(request.body));\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the response body as a Uint8Array, or null for response types that\n * carry no body (HEAD, 204 No Content, 304 Not Modified).\n */\nfunction readResponseBody(\n method: string,\n xhr: XMLHttpRequest,\n): Uint8Array | null {\n if (method === 'HEAD' || xhr.status === 204 || xhr.status === 304) {\n return null;\n }\n\n if (xhr.response instanceof ArrayBuffer) {\n return new Uint8Array(xhr.response);\n }\n\n return null;\n}\n\n/**\n * Converts the adapter request body to a value accepted by `xhr.send()`.\n * `string`, `Uint8Array` (BufferSource), and `FormData` are all valid\n * `XMLHttpRequestBodyInit` values — the cast is safe for the body types\n * the client produces.\n */\nfunction prepareBody(\n body: AdapterRequest['body'],\n): XMLHttpRequestBodyInit | null {\n if (body === null) {\n return null;\n }\n\n return body as XMLHttpRequestBodyInit;\n}\n\n/**\n * Parses the raw header string from `xhr.getAllResponseHeaders()` into a\n * lowercase-keyed record.\n *\n * `getAllResponseHeaders()` returns CRLF-delimited `name: value` lines. When\n * a server sends multiple headers with the same name the browser combines them\n * into a single comma-joined line for most headers, but emits each `Set-Cookie`\n * value as its own line (per spec) to avoid ambiguity with the comma in cookie\n * values. Those are collected here as `string[]` to match the\n * `AdapterResponse.headers` contract.\n *\n * Note: browsers unconditionally block `Set-Cookie` and `Set-Cookie2` from\n * `getAllResponseHeaders()` per the XHR spec, so the `set-cookie` array\n * branch below is effectively unreachable in a real browser — it exists to\n * satisfy the shared `AdapterResponse` type contract.\n */\nfunction parseXHRResponseHeaders(\n raw: string,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n if (!raw) {\n return result;\n }\n\n for (const line of raw.split('\\r\\n')) {\n // Lines are `Name: value` pairs separated by the first colon.\n // indexOf is used (not split) so colons in the value are preserved.\n const colonIndex = line.indexOf(':');\n\n if (colonIndex < 0) {\n // No colon — malformed or trailing empty line; skip\n continue;\n }\n\n // Lowercase to normalize across servers (header names are case-insensitive)\n const key = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n\n if (!key) {\n // Colon at position 0 — no name; skip\n continue;\n }\n\n if (key === 'set-cookie') {\n // Each Set-Cookie directive arrives as its own line — collect into an\n // array so callers never need to split on commas (which are valid inside\n // cookie values). Guarded by the XHR spec in standard browsers, but kept\n // for correctness in legacy environments or platforms with non-standard\n // XHR implementations.\n const existing = result['set-cookie'];\n\n if (existing === undefined) {\n result['set-cookie'] = [value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result['set-cookie'] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Compares URLs as browsers evaluate request destinations:\n * - strips hash fragments (not sent over HTTP)\n * - relies on URL normalization for equivalent forms\n * (default ports, dot segments, encoding normalization, etc.)\n */\nfunction didBrowserFollowRedirect(\n responseURL: string,\n requestURL: string,\n): boolean {\n try {\n const normalizedResponse = new URL(responseURL);\n normalizedResponse.hash = '';\n\n const normalizedRequest = new URL(\n resolveAbsoluteURLForRuntime(requestURL, undefined, true),\n normalizedResponse.href,\n );\n normalizedRequest.hash = '';\n\n return normalizedResponse.href !== normalizedRequest.href;\n } catch {\n // Fallback for non-URL inputs: preserve prior behavior.\n return responseURL !== requestURL;\n }\n}\n"],"mappings":"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","names":[]}