lifecycleion 0.0.9 → 0.0.10

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 (44) hide show
  1. package/README.md +45 -36
  2. package/dist/lib/domain-utils/domain-utils.cjs +1154 -0
  3. package/dist/lib/domain-utils/domain-utils.cjs.map +1 -0
  4. package/dist/lib/domain-utils/domain-utils.d.cts +210 -0
  5. package/dist/lib/domain-utils/domain-utils.d.ts +210 -0
  6. package/dist/lib/domain-utils/domain-utils.js +1112 -0
  7. package/dist/lib/domain-utils/domain-utils.js.map +1 -0
  8. package/dist/lib/http-client/index.cjs +5254 -0
  9. package/dist/lib/http-client/index.cjs.map +1 -0
  10. package/dist/lib/http-client/index.d.cts +372 -0
  11. package/dist/lib/http-client/index.d.ts +372 -0
  12. package/dist/lib/http-client/index.js +5207 -0
  13. package/dist/lib/http-client/index.js.map +1 -0
  14. package/dist/lib/http-client-mock/index.cjs +525 -0
  15. package/dist/lib/http-client-mock/index.cjs.map +1 -0
  16. package/dist/lib/http-client-mock/index.d.cts +129 -0
  17. package/dist/lib/http-client-mock/index.d.ts +129 -0
  18. package/dist/lib/http-client-mock/index.js +488 -0
  19. package/dist/lib/http-client-mock/index.js.map +1 -0
  20. package/dist/lib/http-client-node/index.cjs +1112 -0
  21. package/dist/lib/http-client-node/index.cjs.map +1 -0
  22. package/dist/lib/http-client-node/index.d.cts +43 -0
  23. package/dist/lib/http-client-node/index.d.ts +43 -0
  24. package/dist/lib/http-client-node/index.js +1075 -0
  25. package/dist/lib/http-client-node/index.js.map +1 -0
  26. package/dist/lib/http-client-xhr/index.cjs +323 -0
  27. package/dist/lib/http-client-xhr/index.cjs.map +1 -0
  28. package/dist/lib/http-client-xhr/index.d.cts +23 -0
  29. package/dist/lib/http-client-xhr/index.d.ts +23 -0
  30. package/dist/lib/http-client-xhr/index.js +286 -0
  31. package/dist/lib/http-client-xhr/index.js.map +1 -0
  32. package/dist/lib/lru-cache/index.cjs +274 -0
  33. package/dist/lib/lru-cache/index.cjs.map +1 -0
  34. package/dist/lib/lru-cache/index.d.cts +84 -0
  35. package/dist/lib/lru-cache/index.d.ts +84 -0
  36. package/dist/lib/lru-cache/index.js +249 -0
  37. package/dist/lib/lru-cache/index.js.map +1 -0
  38. package/dist/lib/retry-utils/index.d.cts +3 -23
  39. package/dist/lib/retry-utils/index.d.ts +3 -23
  40. package/dist/types-CUPvmYQ8.d.cts +868 -0
  41. package/dist/types-D_MywcG0.d.cts +23 -0
  42. package/dist/types-D_MywcG0.d.ts +23 -0
  43. package/dist/types-Hw2PUTIT.d.ts +868 -0
  44. package/package.json +45 -3
@@ -0,0 +1,868 @@
1
+ import { R as RetryPolicyOptions } from './types-D_MywcG0.cjs';
2
+
3
+ interface Cookie {
4
+ name: string;
5
+ value: string;
6
+ domain?: string;
7
+ /** True when the cookie came from a Set-Cookie without Domain= and must not match subdomains. */
8
+ hostOnly?: boolean;
9
+ path?: string;
10
+ expires?: Date;
11
+ /** Seconds from {@link createdAt}; no synthetic `expires` when only Max-Age was sent (RFC 6265). */
12
+ maxAge?: number;
13
+ secure?: boolean;
14
+ httpOnly?: boolean;
15
+ sameSite?: 'Strict' | 'Lax' | 'None';
16
+ /** Epoch ms when the cookie was stored. Used with {@link maxAge} to compute expiry. */
17
+ createdAt: number;
18
+ }
19
+ /**
20
+ * Input shape for {@link CookieJar.setCookie}. Identical to {@link Cookie} except
21
+ * `createdAt` is optional — the jar injects `Date.now()` when omitted.
22
+ */
23
+ type CookieInput = Omit<Cookie, 'createdAt'> & {
24
+ createdAt?: number;
25
+ };
26
+ interface CookieJarJSON {
27
+ cookies: Cookie[];
28
+ }
29
+ /**
30
+ * Shareable, standalone cookie jar.
31
+ *
32
+ * Cookies are bucketed by apex domain (via tldts Public Suffix List) for efficient
33
+ * URL lookup — only the relevant bucket is scanned instead of all stored cookies.
34
+ *
35
+ * Validation applied when storing from Set-Cookie headers:
36
+ * - Rejects Domain= values that are recognized public suffixes (e.g. co.uk, com)
37
+ * - Rejects Domain= values that are not a suffix of the request host
38
+ * - Strips leading dots from Domain= per RFC 6265
39
+ *
40
+ * IPs and local hostnames like localhost are never treated as public suffixes.
41
+ */
42
+ declare class CookieJar {
43
+ private buckets;
44
+ /**
45
+ * Stores or updates a cookie. Returns false if the domain is missing or
46
+ * not a valid hostname/IP (e.g. empty string, spaces, garbage input).
47
+ *
48
+ * Valid domains include: hostnames (example.com, localhost, myapp.test),
49
+ * IPv4 (127.0.0.1), and IPv6 ([::1]).
50
+ *
51
+ * For server responses use parseSetCookieHeader — it also enforces PSL
52
+ * validation and domain-suffix checks on top of the syntax check here.
53
+ */
54
+ setCookie(cookie: CookieInput): boolean;
55
+ /**
56
+ * Returns all stored cookies (including possibly expired ones — call
57
+ * clearExpiredCookies first if needed).
58
+ */
59
+ getAllCookies(): Cookie[];
60
+ /**
61
+ * Returns the named cookie applicable for the given URL (domain + path
62
+ * matching, unexpired), or undefined.
63
+ *
64
+ * Applies the same rules as getCookiesFor — domain, path, and expiry are all checked.
65
+ */
66
+ getCookieFor(name: string, url: string): Cookie | undefined;
67
+ /**
68
+ * Returns all domains that have cookies stored, with a count per domain.
69
+ * Domains are stored in canonical form (lowercase hostnames; normalized IP literals).
70
+ */
71
+ getStoredDomains(): Array<{
72
+ domain: string;
73
+ count: number;
74
+ }>;
75
+ /**
76
+ * Parses a Set-Cookie header string and stores the resulting cookie.
77
+ * The request URL is used to infer and validate the domain (PSL + suffix checks).
78
+ */
79
+ parseSetCookieHeader(header: string, url: string): void;
80
+ /**
81
+ * Processes all Set-Cookie headers from a response headers object.
82
+ *
83
+ * Uses the same normalization as `HTTPClient` ({@link normalizeAdapterResponseHeaders}):
84
+ * lowercase keys and merged `set-cookie` lines, so mixed-case adapter output
85
+ * matches `FetchAdapter` / normalized responses.
86
+ */
87
+ processResponseHeaders(headers: Record<string, string | string[]>, url: string): void;
88
+ /**
89
+ * Returns cookies applicable for the given URL (domain + path matching, unexpired).
90
+ * Cookies with the Secure attribute are omitted unless the URL uses the `https:` scheme
91
+ * (RFC 6265 §5.4).
92
+ *
93
+ * Only scans the apex-domain bucket for the URL — O(cookies in that domain)
94
+ * instead of O(all cookies).
95
+ */
96
+ getCookiesFor(url: string): Cookie[];
97
+ /**
98
+ * Returns a `Cookie: name=value; name2=value2` string for the given URL.
99
+ * Uses `getCookiesFor`, so expired cookies are never included (same as RFC
100
+ * behavior on the wire).
101
+ */
102
+ getCookieHeaderString(url: string): string;
103
+ /**
104
+ * Removes expired cookies from the jar. Returns the number of cookies removed.
105
+ */
106
+ clearExpiredCookies(): number;
107
+ /**
108
+ * Removes cookies from the jar.
109
+ *
110
+ * - `clear()` — removes everything
111
+ * - `clear(host, 'hostname')` — removes cookies stored for exactly that hostname,
112
+ * leaving other subdomains untouched. e.g. `clear('api.example.com', 'hostname')`
113
+ * does not touch cookies stored for `example.com`.
114
+ * - `clear(host, 'domain')` — removes all cookies for the entire domain family
115
+ * (apex + all subdomains). e.g. `clear('api.example.com', 'domain')` clears
116
+ * everything in the `example.com` apex bucket.
117
+ */
118
+ clear(): number;
119
+ clear(host: string, scope: 'hostname' | 'domain'): number;
120
+ /**
121
+ * Serializes the jar to JSON.
122
+ */
123
+ toJSON(): CookieJarJSON;
124
+ /**
125
+ * Restores a jar from serialized JSON.
126
+ */
127
+ fromJSON(data: CookieJarJSON): void;
128
+ private cookieKey;
129
+ /** Canonical form for stored cookie domains: lowercase DNS names; canonical IP literals. */
130
+ private normalizeStoredDomain;
131
+ private parseCookieString;
132
+ private pruneEmptyBucket;
133
+ private deleteCookieByIdentity;
134
+ /**
135
+ * RFC 6265 §5.1.4 — default-path from the path portion of the request-uri.
136
+ * E.g. `/admin/settings` → `/admin`; `/admin` or `/` → `/`.
137
+ */
138
+ private defaultCookiePathFromPathname;
139
+ /** Path from Set-Cookie when present and non-empty; otherwise §5.1.4 default-path. */
140
+ private resolvedCookiePath;
141
+ /** Returns the apex (registered) domain for bucketing.
142
+ * Falls back to hostname for IPs and local hostnames like localhost. */
143
+ private apexFor;
144
+ private getOrCreateBucket;
145
+ private storeParsed;
146
+ /**
147
+ * Returns a canonical lowercase IP literal for bucketing and matching, or null if
148
+ * `host` is not syntactically a valid IPv4 or IPv6 host string (bracketed or not).
149
+ *
150
+ * Uses the URL parser for IPv6 literals so validation does not depend on tldts
151
+ * `isIp`, which is unreliable for bracketed IPv6 and false for unbracketed `::1`
152
+ * in some releases (see tldts#2288).
153
+ */
154
+ private tryCanonicalIPLiteral;
155
+ private unbracketHost;
156
+ /** Validates domain syntax for setCookie. Accepts hostnames (example.com,
157
+ * localhost, myapp.test), IPv4, and bracketed IPv6 ([::1]). Rejects empty
158
+ * strings, spaces, and other garbage. Does NOT enforce PSL — use
159
+ * parseSetCookieHeader for server responses. */
160
+ private isSyntaxValidDomain;
161
+ /** Returns true if the domain is a recognized public suffix (e.g. co.uk, com).
162
+ * IPs and local hostnames like localhost are not rejected. */
163
+ private isPublicSuffix;
164
+ private isExpired;
165
+ private domainMatches;
166
+ private hostOnlyDomainMatches;
167
+ /**
168
+ * RFC 6265 §5.1.4 — request-path path-matches cookie-path when:
169
+ * (1) identical, (2) cookie-path is a prefix and ends with `/`, or
170
+ * (3) cookie-path is a prefix and the next request-path character is `/`.
171
+ *
172
+ * A single `startsWith(cookiePath + '/')` only covers (1)+(3) and breaks (2):
173
+ * e.g. cookie `/api/` must match `/api/users` without requiring `/api//`.
174
+ */
175
+ private pathMatches;
176
+ }
177
+
178
+ type AdapterType = 'fetch' | 'xhr' | 'node' | 'mock';
179
+ type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
180
+ type ContentType = 'json' | 'text' | 'binary';
181
+ /**
182
+ * Public query value shape compatible with `qs.parse()` output, so consumers
183
+ * do not need `@types/qs` installed just to use this package's declarations.
184
+ */
185
+ type QueryValue = string | QueryObject | QueryValue[] | undefined;
186
+ interface QueryObject {
187
+ [key: string]: QueryValue;
188
+ }
189
+ interface HTTPAdapter {
190
+ send(request: AdapterRequest): Promise<AdapterResponse>;
191
+ getType(): AdapterType;
192
+ }
193
+ interface AdapterRequest {
194
+ requestURL: string;
195
+ method: HTTPMethod;
196
+ /** Lowercase-keyed outbound headers for this attempt. Some adapters materialize string[] values at send time. */
197
+ headers: Record<string, string | string[]>;
198
+ body?: string | Uint8Array | FormData | null;
199
+ signal?: AbortSignal;
200
+ onUploadProgress?: (event: AdapterProgressEvent) => void;
201
+ onDownloadProgress?: (event: AdapterProgressEvent) => void;
202
+ /**
203
+ * NodeAdapter only. HTTPClient rejects non-node adapters before dispatch if
204
+ * this is set. Called after response headers arrive on a 200 response.
205
+ * Return a writable stream to pipe the body into it, or null to cancel.
206
+ */
207
+ streamResponse?: StreamResponseFactory;
208
+ /** Passed by the client so NodeAdapter can populate StreamResponseInfo. */
209
+ attemptNumber?: number;
210
+ /** Passed by the client so NodeAdapter can populate StreamResponseInfo. */
211
+ requestID?: string;
212
+ }
213
+ interface AdapterResponse {
214
+ status: number;
215
+ /**
216
+ * Set by adapters when a redirect response was encountered.
217
+ *
218
+ * - **NodeAdapter / MockAdapter**: set on real 3xx responses. Location is
219
+ * accessible, so HTTPClient's redirect loop can follow normally.
220
+ *
221
+ * - **FetchAdapter**: uses `redirect: 'manual'`.
222
+ * - In server runtimes (Bun, Node) this returns the real 3xx with a Location
223
+ * header, so HTTPClient's redirect loop works normally.
224
+ *
225
+ * - In a browser, CORS constraints cause `redirect: 'manual'` to yield an
226
+ * opaque response (status 0, no Location). Browser adapters require
227
+ * `followRedirects: false`, so a detected redirect always results in
228
+ * `redirect_disabled`.
229
+ *
230
+ * - **XHRAdapter**: the browser always follows redirects with no opt-out.
231
+ * Detected after the fact by comparing `xhr.responseURL` to the original
232
+ * URL; resolves as status 0 with `wasRedirectDetected: true`. Same
233
+ * HTTPClient routing as FetchAdapter.
234
+ */
235
+ wasRedirectDetected?: boolean;
236
+ /**
237
+ * Redirect target when the adapter can determine it, even if the client
238
+ * does not follow that redirect itself.
239
+ */
240
+ detectedRedirectURL?: string;
241
+ /**
242
+ * True when the adapter wants the client to treat this as a transport-level
243
+ * failure. Adapters may still preserve a diagnostic status code such as 495,
244
+ * or use `status: 0` for generic transport failures. The client routes these
245
+ * through the failed/error path and sets `HTTPResponse.isFailed: true`.
246
+ */
247
+ isTransportError?: boolean;
248
+ /**
249
+ * Set to `false` when the adapter knows the request/response progressed far
250
+ * enough that replay is unsafe, even if `status` would normally be retried
251
+ * by the client (for example a mid-upload socket write failure after some
252
+ * bytes may already have left the process).
253
+ */
254
+ isRetryable?: boolean;
255
+ /**
256
+ * Final request headers actually used by the adapter after adapter-local
257
+ * mutations (for example multipart Content-Type/Content-Length added by the
258
+ * Node adapter). When present, HTTPClient merges these into the observer-
259
+ * facing AttemptRequest snapshot so response/error observers see the real
260
+ * outgoing headers rather than only the pre-adapter request shape.
261
+ */
262
+ effectiveRequestHeaders?: Record<string, string | string[]>;
263
+ /**
264
+ * Most headers are `string`. `set-cookie` is always `string[]` (HTTP spec,
265
+ * each cookie is a separate header line).
266
+ *
267
+ * Adapters may use any header casing, `HTTPClient` normalizes keys to
268
+ * lowercase before redirect handling, cookies, and the public `HTTPResponse`.
269
+ */
270
+ headers: Record<string, string | string[]>;
271
+ /** Raw response bytes. Higher layers decode/parse based on content-type. */
272
+ body: Uint8Array | null;
273
+ /**
274
+ * Set by NodeAdapter when the body was piped to a StreamResponseFactory writable.
275
+ * The client skips body parsing and sets isStreamed: true on HTTPResponse.
276
+ */
277
+ isStreamed?: boolean;
278
+ /**
279
+ * Set by NodeAdapter when response-body delivery fails after headers arrive
280
+ * (for example buffered-download truncation, writable errors, or upstream
281
+ * response-stream errors). The client treats this as a terminal stream
282
+ * failure: isStreamError: true. The real HTTP status is preserved (not
283
+ * zeroed) so observers can tell the server responded but body delivery still
284
+ * failed locally/in-flight.
285
+ */
286
+ isStreamError?: boolean;
287
+ /**
288
+ * Optional sub-classification for `isStreamError` so the client can surface a
289
+ * more precise `HTTPClientError.code` while keeping `HTTPResponse`
290
+ * transport-agnostic.
291
+ */
292
+ streamErrorCode?: 'stream_write_error' | 'stream_response_error';
293
+ /**
294
+ * Underlying error for an absorbed adapter failure. Populated for transport
295
+ * failures the adapter resolves instead of throwing, and for streamed-body
296
+ * failures carried via `isStreamError`.
297
+ */
298
+ errorCause?: Error;
299
+ }
300
+ /**
301
+ * Minimal write-capable stream interface. Structurally matches Node.js Writable
302
+ * without importing from 'node:stream', keeping this file isomorphic.
303
+ */
304
+ interface WritableLike {
305
+ write(chunk: Uint8Array | string, cb?: (err?: Error | null) => void): boolean;
306
+ end(cb?: () => void): void;
307
+ on(event: 'error', listener: (err: Error) => void): this;
308
+ on(event: 'close', listener: () => void): this;
309
+ on(event: 'drain', listener: () => void): this;
310
+ once(event: 'drain', listener: () => void): this;
311
+ destroy(error?: Error): void;
312
+ }
313
+ /**
314
+ * Info passed to a StreamResponseFactory. Status is always 200 — the factory
315
+ * is never called for any other status code.
316
+ */
317
+ interface StreamResponseInfo {
318
+ /** Always 200 — factory is not called for any other status. */
319
+ status: 200;
320
+ /** Response headers for this attempt. Most values are string; set-cookie is string[]. */
321
+ headers: Record<string, string | string[]>;
322
+ /** Fully resolved request URL for this attempt. */
323
+ url: string;
324
+ /** Which attempt this is (1-based). Increments on retry. */
325
+ attempt: number;
326
+ /** ULID assigned to this send() call. */
327
+ requestID: string;
328
+ }
329
+ /**
330
+ * Context passed alongside StreamResponseInfo. Provides an attempt-scoped
331
+ * AbortSignal that fires on cancel, timeout, or stream write failure — so
332
+ * cleanup logic (delete partial file, close import stream, etc.) can be
333
+ * co-located with the stream setup code instead of scattered across observers.
334
+ */
335
+ interface StreamResponseContext {
336
+ /** Fires on cancel, timeout, or stream write failure for this attempt. */
337
+ signal: AbortSignal;
338
+ }
339
+ /**
340
+ * Returned from a StreamResponseFactory to cancel the stream with an optional reason.
341
+ * Equivalent to returning null, but allows attaching a reason to HTTPClientError.cancelReason.
342
+ */
343
+ interface StreamResponseCancel {
344
+ cancel: true;
345
+ reason?: string;
346
+ }
347
+ /**
348
+ * Factory called by NodeAdapter after response headers arrive on a 200.
349
+ * Return a WritableLike to pipe the body into it, null to cancel, or
350
+ * `{ cancel: true, reason?: string }` to cancel with an optional reason string
351
+ * surfaced on HTTPClientError.cancelReason.
352
+ * May be async.
353
+ */
354
+ type StreamResponseFactory = (info: StreamResponseInfo, context: StreamResponseContext) => WritableLike | null | StreamResponseCancel | Promise<WritableLike | null | StreamResponseCancel>;
355
+ /**
356
+ * Raw progress event emitted by adapters. Does not include `attemptNumber` or
357
+ * `hopNumber` — those are injected by `HTTPClient` when forwarding to consumers.
358
+ */
359
+ interface AdapterProgressEvent {
360
+ loaded: number;
361
+ total: number;
362
+ /** 0–1, or -1 if total unknown */
363
+ progress: number;
364
+ }
365
+ interface HTTPProgressEvent extends AdapterProgressEvent {
366
+ attemptNumber: number;
367
+ /** Present only during redirect hops (1 = first redirect, 2 = second, etc.). */
368
+ hopNumber?: number;
369
+ }
370
+ interface AttemptStartEvent {
371
+ attemptNumber: number;
372
+ isRetry: boolean;
373
+ requestID: string;
374
+ /**
375
+ * The original resolved URL for this `send()` before redirect follow-ups.
376
+ *
377
+ * During `initial` interceptors this is the pre-interceptor resolved URL.
378
+ * In later phases it matches {@link HTTPResponse.initialURL}. This is not
379
+ * the URL of the current adapter attempt — see
380
+ * {@link InterceptedRequest.requestURL} and `redirect.to` for hop context.
381
+ */
382
+ initialURL: string;
383
+ /**
384
+ * Set for adapter attempts that are part of a redirect follow-up (1 = first hop).
385
+ * Aligns with {@link HTTPProgressEvent.hopNumber}.
386
+ */
387
+ hopNumber?: number;
388
+ /**
389
+ * When `hopNumber` is set, the redirect hop this attempt belongs to — same fields as
390
+ * `RequestPhase` `redirect` / `retry.redirect` for observers.
391
+ */
392
+ redirect?: RedirectHopInfo;
393
+ }
394
+ interface AttemptEndEvent {
395
+ attemptNumber: number;
396
+ isRetry: boolean;
397
+ willRetry: boolean;
398
+ /** Present only when a retry has been scheduled after this attempt. */
399
+ nextRetryDelayMS?: number;
400
+ /** Epoch ms for the scheduled retry, when a retry has been scheduled. */
401
+ nextRetryAt?: number;
402
+ status: number;
403
+ requestID: string;
404
+ /** Same as {@link AttemptStartEvent.initialURL}. */
405
+ initialURL: string;
406
+ hopNumber?: number;
407
+ redirect?: RedirectHopInfo;
408
+ }
409
+ interface HTTPClientConfig {
410
+ adapter?: HTTPAdapter;
411
+ /**
412
+ * Origin / prefix for relative request paths (`/api/...`, `v1/...`, etc.).
413
+ *
414
+ * Omit or leave unset only when every {@link BaseHTTPClient.request} path is
415
+ * already absolute, or when you resolve URLs yourself.
416
+ *
417
+ * Relative paths are appended to this value. Full `http(s)://…` paths (and
418
+ * `//host/…`) skip concatenation with `baseURL`; see JSDoc on
419
+ * `BaseHTTPClient.request` and `buildURL` in `utils.ts`.
420
+ */
421
+ baseURL?: string;
422
+ defaultHeaders?: Record<string, string | string[]>;
423
+ timeout?: number;
424
+ cookieJar?: CookieJar | null;
425
+ retryPolicy?: RetryPolicyOptions;
426
+ includeRequestID?: boolean;
427
+ includeAttemptHeader?: boolean;
428
+ userAgent?: string;
429
+ followRedirects?: boolean;
430
+ maxRedirects?: number;
431
+ }
432
+ interface SubClientConfig extends Partial<HTTPClientConfig> {
433
+ /**
434
+ * How sub-client `defaultHeaders` should interact with inherited defaults.
435
+ * Default: `'replace'`.
436
+ *
437
+ * Use `'merge'` to preserve inherited defaults and layer new headers on top.
438
+ *
439
+ * If no new `defaultHeaders` are provided, `'merge'` keeps the inherited
440
+ * defaults as-is.
441
+ */
442
+ defaultHeadersStrategy?: 'replace' | 'merge';
443
+ }
444
+ interface HTTPRequestOptions {
445
+ headers?: Record<string, string | string[]>;
446
+ params?: Record<string, unknown>;
447
+ body?: unknown;
448
+ timeout?: number;
449
+ signal?: AbortSignal;
450
+ retryPolicy?: RetryPolicyOptions | null;
451
+ label?: string;
452
+ onUploadProgress?: (event: HTTPProgressEvent) => void;
453
+ onDownloadProgress?: (event: HTTPProgressEvent) => void;
454
+ onAttemptStart?: (event: AttemptStartEvent) => void;
455
+ onAttemptEnd?: (event: AttemptEndEvent) => void;
456
+ /**
457
+ * NodeAdapter only. Called after response headers arrive on a 200 response.
458
+ * Return a WritableLike to pipe the body to it, or null to cancel.
459
+ * HTTPClient rejects non-node adapters before dispatch if this is set.
460
+ */
461
+ streamResponse?: StreamResponseFactory;
462
+ }
463
+ interface HTTPResponse<T = unknown> {
464
+ status: number;
465
+ /**
466
+ * Lowercase keys (normalized by `HTTPClient` from adapter output).
467
+ * Most values are `string`; `set-cookie` is `string[]`.
468
+ */
469
+ headers: Record<string, string | string[]>;
470
+ body: T;
471
+ contentType: ContentType;
472
+ isJSON: boolean;
473
+ isText: boolean;
474
+ isCancelled: boolean;
475
+ isTimeout: boolean;
476
+ isNetworkError: boolean;
477
+ /**
478
+ * True when this response settled through the client's failed/error path.
479
+ *
480
+ * In practice this is true for responses flagged as `isCancelled`,
481
+ * `isTimeout`, `isNetworkError`, `isStreamError`, or adapter
482
+ * `isTransportError`, as well as other client-level failures that resolve to
483
+ * `status: 0` (for example request setup, interceptor, and redirect
484
+ * control-flow failures). For adapter-originated transport failures,
485
+ * `isTransportError` is the explicit signal; bare `status: 0` is also
486
+ * treated as a transport failure by the client.
487
+ *
488
+ * Ordinary HTTP responses, including HTTP error statuses like 4xx/5xx,
489
+ * remain `false`. This tracks client-level failure handling, not HTTP
490
+ * status class.
491
+ */
492
+ isFailed: boolean;
493
+ isParseError: boolean;
494
+ /**
495
+ * Request URL for this `send()` after `initial` interceptors, before any
496
+ * redirect follow-ups. Same string as {@link AttemptStartEvent.initialURL} on attempt hooks.
497
+ * Real transport adapters use an absolute http(s) URL here. MockAdapter
498
+ * path-only requests without a client `baseURL` are materialized as
499
+ * `http://localhost/...` before interceptors run.
500
+ */
501
+ initialURL: string;
502
+ /**
503
+ * URL of the request that produced this response (last adapter attempt after redirects).
504
+ * Equals {@link HTTPResponse.initialURL} when no redirect occurred.
505
+ */
506
+ requestURL: string;
507
+ /** True when a redirect response was encountered at any point (followed or not). */
508
+ wasRedirectDetected: boolean;
509
+ /** True when HTTPClient's own redirect loop followed at least one hop. */
510
+ wasRedirectFollowed: boolean;
511
+ /** Redirect target when known but not followed by the client loop. */
512
+ detectedRedirectURL?: string;
513
+ /**
514
+ * Redirect target URLs recorded during redirect handling, in order.
515
+ * The current detected target may appear here before its follow-up request is
516
+ * dispatched. If a redirect-phase interceptor rewrites that target and
517
+ * redirect handling continues, later entries reflect the rewritten URL.
518
+ */
519
+ redirectHistory: string[];
520
+ requestID: string;
521
+ adapterType: AdapterType;
522
+ /**
523
+ * True when the response body was piped to a StreamResponseFactory writable.
524
+ * body is null in this case — do not try to parse it.
525
+ */
526
+ isStreamed: boolean;
527
+ /**
528
+ * True when streamed body delivery fails after headers arrive (disk full,
529
+ * writable destroyed, upstream response-stream error, etc.). The request
530
+ * resolves as a failed response, with the real HTTP status preserved so
531
+ * observers can distinguish a post-header streaming failure from a transport
532
+ * failure (status: 0). Stream setup/factory failures do not set
533
+ * `isStreamError`.
534
+ */
535
+ isStreamError: boolean;
536
+ }
537
+ type ErrorCode = 'network_error' | 'timeout' | 'cancelled' | 'redirect_disabled' | 'redirect_loop' | 'request_setup_error' | 'adapter_error' | 'interceptor_error' | 'stream_write_error' | 'stream_response_error' | 'stream_setup_error';
538
+ interface HTTPClientError {
539
+ code: ErrorCode;
540
+ message: string;
541
+ cause?: Error;
542
+ /**
543
+ * Original resolved URL for this `send()` before redirects.
544
+ *
545
+ * During `initial` interceptors this is the pre-interceptor resolved URL.
546
+ * In later phases it matches {@link HTTPResponse.initialURL} and
547
+ * {@link AttemptStartEvent.initialURL}.
548
+ * Real transport adapters use an absolute http(s) URL here. MockAdapter
549
+ * path-only requests without a client `baseURL` are materialized as
550
+ * `http://localhost/...` before interceptors run.
551
+ */
552
+ initialURL: string;
553
+ /** URL of the last request attempt when the error was produced. */
554
+ requestURL: string;
555
+ /** True when a redirect response was encountered at any point (followed or not). */
556
+ wasRedirectDetected: boolean;
557
+ /** True when HTTPClient's own redirect loop followed at least one hop before the error. */
558
+ wasRedirectFollowed: boolean;
559
+ /** Redirect target when known but not followed by the client loop. */
560
+ detectedRedirectURL?: string;
561
+ /**
562
+ * Redirect target URLs recorded during redirect handling, in order.
563
+ * The current detected target may appear here before its follow-up request is
564
+ * dispatched. If a redirect-phase interceptor rewrites that target and
565
+ * redirect handling continues, later entries reflect the rewritten URL.
566
+ */
567
+ redirectHistory: string[];
568
+ requestID: string;
569
+ isTimeout: boolean;
570
+ /** True when a retry policy was active and all attempts were exhausted before a response was received. */
571
+ isRetriesExhausted: boolean;
572
+ /**
573
+ * Optional reason string when the request was cancelled with an explicit string reason.
574
+ * Set by: a request interceptor returning `{ cancel: true, reason: '...' }`, a
575
+ * StreamResponseFactory returning `{ cancel: true, reason: '...' }`, or an AbortSignal
576
+ * aborted via `controller.abort('reason')`. Undefined when no explicit string reason
577
+ * was provided (e.g. `controller.abort()` with no argument, `builder.cancel()`).
578
+ */
579
+ cancelReason?: string;
580
+ }
581
+ /**
582
+ * **Phases vs hooks (one mental model)**
583
+ *
584
+ * - **`retry` and `redirect` are both phases** on the same `RequestPhaseName` list. They
585
+ * mean “something intermediate happened before this `send()` finishes,” not “only one
586
+ * kind of thing counts as a phase.” The names differ because the **payload** differs:
587
+ * retries are policy- and attempt-scoped (`attempt`, `maxAttempts`), while redirects are
588
+ * HTTP hop-scoped (`hop`, `from`, `to`, `statusCode`). Same idea as having both
589
+ * `click` and `keydown` in the DOM, different events but the same observer mechanism.
590
+ *
591
+ * - **Global (client-scoped) monitoring:** `addResponseObserver` / `addErrorObserver` on
592
+ * `HTTPClient`, with optional `phases` (plain arrays like `['retry', 'redirect', 'final']`).
593
+ * One callback with
594
+ * `phases: ['retry', 'redirect']` runs **once per matching event**, e.g. a `send()` that
595
+ * redirects once then retries once invokes the handler twice with different `phase.type`,
596
+ * never twice for the same adapter response (a status is either a redirect hop or a retry
597
+ * trigger, not both). The filter list is **OR** on `phase.type`.
598
+ *
599
+ * - **Policy retry (`type: ‘retry’`):** fires once per reattempt. Interceptors and observers
600
+ * always see the URL being retried, which is the redirect target when a redirect preceded
601
+ * the retry. The optional `phase.redirect` field is metadata that tells you which redirect
602
+ * hop led to this retry (`{ hop, from, to, statusCode }`), so you don’t have to track
603
+ * that yourself.
604
+ *
605
+ * - **Per-request (builder-scoped) monitoring:** `onAttemptStart`, `onAttemptEnd`, and
606
+ * progress callbacks fire only for that builder’s `send()`. They do not replace global
607
+ * observers. They are for when you want lifecycle tied to one call without registering
608
+ * on the client. Each **adapter** attempt on the initial URL and on every redirect
609
+ * follow-up (including policy retries on a hop) invokes start/end; `initialURL` on
610
+ * those events matches {@link HTTPResponse.initialURL}. Redirect attempts include
611
+ * `hopNumber` and `redirect` ({@link RedirectHopInfo}) matching observer phases.
612
+ *
613
+ * - **`initial`** is for **request** interceptors (mutate before send), not response/error
614
+ * observers. Settlement-only errors (interceptor throw mid-redirect/retry setup) still
615
+ * use **`final`** on error observers by design.
616
+ *
617
+ * **Per-handler phase subsets:**
618
+ * - Interceptors see: `initial`, `retry`, `redirect` (never `final`)
619
+ * - Response observers see: `retry`, `redirect`, `final` (never `initial`)
620
+ * - Error observers see: `retry`, `final` (never `initial` or `redirect` — redirect errors
621
+ * surface as `final`, retry errors on redirect hops carry `redirect` inside the `retry` phase)
622
+ */
623
+ type RequestPhaseName = 'initial' | 'retry' | 'redirect' | 'final';
624
+ /** Phases that can reach a {@link RequestInterceptor}. */
625
+ type InterceptorPhaseName = 'initial' | 'retry' | 'redirect';
626
+ /** Phases that can reach a {@link ResponseObserver}. */
627
+ type ResponseObserverPhaseName = 'retry' | 'redirect' | 'final';
628
+ /** Phases that can reach an {@link ErrorObserver}. */
629
+ type ErrorObserverPhaseName = 'retry' | 'final';
630
+ /**
631
+ * One HTTP redirect hop. The **`redirect`** phase and **`retry.redirect`** (policy retry on a
632
+ * post-redirect URL) both use this shape.
633
+ *
634
+ * **`from` / `to`:** Intended to be absolute URLs (the {@link InterceptedRequest.requestURL}
635
+ * that received the redirect response, and the next {@link InterceptedRequest.requestURL}).
636
+ * `Location` is resolved against `from`, then normalized with the client `baseURL` when
637
+ * needed. Without a `baseURL`, path-only strings may remain as paths.
638
+ */
639
+ type RedirectHopInfo = {
640
+ hop: number;
641
+ from: string;
642
+ to: string;
643
+ statusCode: number;
644
+ };
645
+ /** Discriminated union describing the current phase of a request lifecycle. */
646
+ type RequestPhase = {
647
+ type: 'initial';
648
+ } | {
649
+ type: 'retry';
650
+ attempt: number;
651
+ maxAttempts: number;
652
+ /**
653
+ * When set, this policy retry applies after this redirect hop (same fields as
654
+ * `type: 'redirect'`). Omit when the reattempt still uses the original request URL.
655
+ */
656
+ redirect?: RedirectHopInfo;
657
+ } | ({
658
+ type: 'redirect';
659
+ } & RedirectHopInfo) | {
660
+ type: 'final';
661
+ };
662
+ /** Narrowed phase union for {@link RequestInterceptor} — excludes `final`. */
663
+ type InterceptorPhase = Extract<RequestPhase, {
664
+ type: InterceptorPhaseName;
665
+ }>;
666
+ /** Narrowed phase union for {@link ResponseObserver} — excludes `initial`. */
667
+ type ResponseObserverPhase = Extract<RequestPhase, {
668
+ type: ResponseObserverPhaseName;
669
+ }>;
670
+ /** Narrowed phase union for {@link ErrorObserver} — the phases actually delivered to the callback (excludes `initial` and `redirect`). */
671
+ type ErrorObserverPhase = Extract<RequestPhase, {
672
+ type: ErrorObserverPhaseName;
673
+ }>;
674
+ interface PhaseFilter {
675
+ methods?: HTTPMethod[];
676
+ hosts?: string[];
677
+ /**
678
+ * Match against the request scheme (`'http'` or `'https'`). In practice
679
+ * `requestURL` is absolute whenever the request could be resolved before
680
+ * dispatch. `MockAdapter` path-only requests without a client `baseURL`
681
+ * are materialized as `http://localhost/...`, browser adapters resolve
682
+ * against `window.location`, and non-mock adapters still require absolute
683
+ * URLs at validation time. The scheme check is skipped only when
684
+ * `requestURL` is absent.
685
+ */
686
+ schemes?: ('http' | 'https')[];
687
+ }
688
+ /**
689
+ * Filter for request interceptors.
690
+ * Default phases: `['initial']`. Only runs before the first request attempt.
691
+ * Set `phases` to `['initial', 'retry', 'redirect']` to re-run before every attempt.
692
+ *
693
+ * On a redirect follow-up, the first adapter attempt uses phase `redirect`. Further
694
+ * attempts on that same URL (policy retry) use `retry` with the same `redirect` object
695
+ * shape nested under `retry.redirect`.
696
+ */
697
+ interface RequestInterceptorFilter extends PhaseFilter {
698
+ /**
699
+ * Which events run this interceptor: **OR** over phase type. Valid values: `initial`,
700
+ * `retry`, `redirect`. (`final` never reaches interceptors.)
701
+ *
702
+ * Default: `['initial']`. Pass `['initial', 'retry', 'redirect']` to re-run on every attempt.
703
+ * Pass `[]` to match all phases.
704
+ */
705
+ phases?: InterceptorPhaseName[];
706
+ /** Match against the outgoing request body. */
707
+ bodyContainsKeys?: string[];
708
+ }
709
+ /**
710
+ * Filter for response observers. See {@link RequestPhaseName} for how `retry` and
711
+ * `redirect` fit together as intermediate phases.
712
+ *
713
+ * Default phases: `[‘final’]`. Terminal **HTTP** response for this `send()` only.
714
+ *
715
+ * - **`’retry’`**: the adapter returned a **retryable HTTP status** (including `0`
716
+ * when it is in the client’s retryable set) and a further attempt will run. Phase is
717
+ * `{ type: ‘retry’, attempt, maxAttempts, redirect? }` (aligned with
718
+ * `onAttemptEnd.attemptNumber`, with `redirect` only for retries on a post-redirect URL).
719
+ * If the adapter **throws** but a retry will follow, that path uses **error**
720
+ * observers with the same phase shape, not response observers.
721
+ * - **`’redirect’`**: each redirect **response** before following `Location`. Phase is
722
+ * `{ type: ‘redirect’, hop, from, to, statusCode }` (see {@link RedirectHopInfo}).
723
+ *
724
+ * When `send()` **settles** with `status === 0`, **error** observers run with phase
725
+ * `final` only. **Before** that, each retryable `0` response in a retry cycle can
726
+ * still invoke **response** observers with phase `retry` when your filter includes it.
727
+ */
728
+ interface ResponseObserverFilter extends PhaseFilter {
729
+ /**
730
+ * Which events run this observer: **OR** over phase type. Valid values: `retry`,
731
+ * `redirect`, `final`. (`initial` never reaches response observers.)
732
+ *
733
+ * Default: `['final']`. Pass `[]` to match all phases.
734
+ */
735
+ phases?: ResponseObserverPhaseName[];
736
+ statusCodes?: number[];
737
+ /**
738
+ * Match against the parsed response content type category (`json`, `text`, `binary`).
739
+ */
740
+ contentTypes?: ContentType[];
741
+ /**
742
+ * Match against the raw `content-type` header, with optional wildcard subtype patterns
743
+ * such as `image/*`.
744
+ */
745
+ contentTypeHeaders?: string[];
746
+ /**
747
+ * Match against the decoded response body.
748
+ */
749
+ bodyContainsKeys?: string[];
750
+ }
751
+ /**
752
+ * Filter for error observers. Response-only fields like `statusCodes` and
753
+ * `bodyContainsKeys` are excluded.
754
+ *
755
+ * Default phases: `['final']`. Fires when the request settles with an error,
756
+ * including failures before any adapter call (for example an interceptor
757
+ * throw or interceptor cancel). Interceptor failures during `retry` or
758
+ * `redirect` interceptor phases are also reported as `final`, not as those
759
+ * phase names. That matches settlement semantics, not “which interceptor
760
+ * phase threw.”
761
+ *
762
+ * Include `'retry'` in `phases` to also run when the **adapter** throws but a retry
763
+ * will follow. Same `HTTPClientError` shape as terminal adapter errors,
764
+ * with `isRetriesExhausted: false` and phase
765
+ * `{ type: 'retry', attempt, maxAttempts, redirect? }`. **Interceptor** throws
766
+ * (any phase, including `retry` / `redirect`) never emit this: they abort the chain
767
+ * and only **`final`** error observers run, as in the paragraph above. Retryable **HTTP
768
+ * status** responses (including `0` when retried) use **response** observers for this
769
+ * phase name, not error observers.
770
+ */
771
+ interface ErrorObserverFilter extends PhaseFilter {
772
+ /**
773
+ * Which events run this observer: **OR** over phase type. Only `retry` and
774
+ * `final` are valid here — `initial` and `redirect` never reach error
775
+ * observers (redirect errors surface as `final`; retry errors on redirect
776
+ * hops carry `redirect` context inside the `retry` phase).
777
+ *
778
+ * Default: `['final']`. Pass `[]` to match all phases.
779
+ */
780
+ phases?: ErrorObserverPhaseName[];
781
+ }
782
+ /**
783
+ * Signal returned by an interceptor to cancel the request.
784
+ * When returned, the request is aborted and a 'cancelled' error is produced.
785
+ */
786
+ interface InterceptorCancel {
787
+ cancel: true;
788
+ reason?: string;
789
+ }
790
+ /**
791
+ * Additional context passed to request interceptors so they can see the full
792
+ * request chain without having to track it themselves.
793
+ */
794
+ interface RequestInterceptorContext {
795
+ /**
796
+ * Original resolved URL for this `send()` before redirects.
797
+ *
798
+ * During `initial` interceptors this is the pre-interceptor resolved URL.
799
+ * In later phases it matches {@link HTTPResponse.initialURL} and
800
+ * {@link AttemptStartEvent.initialURL}.
801
+ */
802
+ initialURL: string;
803
+ /**
804
+ * Redirect targets already recorded for this send, in order.
805
+ * During redirect-phase interceptors this includes the current detected
806
+ * target before any rewrite returned from that interceptor.
807
+ */
808
+ redirectHistory: string[];
809
+ /**
810
+ * ULID assigned to this `send()` call.
811
+ * Matches {@link HTTPResponse.requestID} and {@link HTTPClientError.requestID}.
812
+ */
813
+ requestID: string;
814
+ /**
815
+ * The adapter attempt number that will be dispatched after this interceptor
816
+ * chain completes (1-based). Increments on each retry across all redirect hops.
817
+ */
818
+ attemptNumber: number;
819
+ }
820
+ type RequestInterceptor = (request: InterceptedRequest, phase: InterceptorPhase, context: RequestInterceptorContext) => InterceptedRequest | InterceptorCancel | null | Promise<InterceptedRequest | InterceptorCancel | null>;
821
+ interface InterceptedRequest {
822
+ /**
823
+ * Pending request shape seen by request interceptors before body serialization.
824
+ * `requestURL` changes on redirect follow-ups and is absolute before dispatch.
825
+ */
826
+ requestURL: string;
827
+ method: HTTPMethod;
828
+ headers: Record<string, string | string[]>;
829
+ body?: unknown;
830
+ }
831
+ /**
832
+ * Finalized observer-facing snapshot for one adapter attempt, after
833
+ * headers/body have been prepared for dispatch.
834
+ */
835
+ type AttemptRequest = Omit<AdapterRequest, 'headers' | 'signal' | 'onUploadProgress' | 'onDownloadProgress' | 'streamResponse'> & {
836
+ /**
837
+ * Final request headers visible to response/error observers. These are
838
+ * lowercase-keyed. Most values are `string`, but adapter-local snapshots may
839
+ * preserve repeated request headers as `string[]` when the underlying runtime
840
+ * supports them.
841
+ */
842
+ headers: Record<string, string | string[]>;
843
+ /**
844
+ * Post-interceptor body before attempt materialization. This is useful for
845
+ * observers that need the semantic payload as well as the adapter-facing body.
846
+ */
847
+ rawBody?: unknown;
848
+ /** Effective per-attempt timeout in ms. */
849
+ timeout?: number;
850
+ /**
851
+ * 1-based attempt number for this adapter call. Increments on each retry.
852
+ * Undefined on best-effort snapshots produced before any adapter attempt is
853
+ * dispatched (e.g. interceptor errors, pre-send cancellations).
854
+ */
855
+ attemptNumber?: number;
856
+ /**
857
+ * ULID assigned to this {@link BaseHTTPClient.request} `send()` call.
858
+ * Matches {@link HTTPResponse.requestID} and {@link HTTPClientError.requestID}.
859
+ * Undefined on best-effort snapshots produced before the request ID is assigned
860
+ * (should not occur in practice).
861
+ */
862
+ requestID?: string;
863
+ };
864
+ type ResponseObserver = (response: HTTPResponse, request: AttemptRequest, phase: ResponseObserverPhase) => void | Promise<void>;
865
+ type ErrorObserver = (error: HTTPClientError, request: AttemptRequest, phase: ErrorObserverPhase) => void | Promise<void>;
866
+ type RequestState = 'pending' | 'sending' | 'waiting_for_retry' | 'completed' | 'cancelled' | 'failed';
867
+
868
+ export { type AdapterType as A, type ErrorObserverPhaseName as B, type ContentType as C, type InterceptorPhase as D, type ErrorObserver as E, type ResponseObserverPhase as F, type ErrorObserverPhase as G, type HTTPAdapter as H, type InterceptorCancel as I, type RedirectHopInfo as J, type StreamResponseInfo as K, type StreamResponseContext as L, type StreamResponseCancel as M, type Cookie as N, type CookieInput as O, type CookieJarJSON as P, type QueryObject as Q, type RequestState as R, type StreamResponseFactory as S, type WritableLike as W, type AdapterRequest as a, type AdapterResponse as b, type HTTPMethod as c, type HTTPRequestOptions as d, type HTTPResponse as e, type HTTPClientError as f, type HTTPProgressEvent as g, type AttemptStartEvent as h, type AttemptEndEvent as i, type HTTPClientConfig as j, type RequestInterceptor as k, type RequestInterceptorFilter as l, type ResponseObserver as m, type ResponseObserverFilter as n, type ErrorObserverFilter as o, type SubClientConfig as p, CookieJar as q, type QueryValue as r, type ErrorCode as s, type RequestInterceptorContext as t, type InterceptedRequest as u, type AttemptRequest as v, type RequestPhase as w, type RequestPhaseName as x, type InterceptorPhaseName as y, type ResponseObserverPhaseName as z };