@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.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 (263) hide show
  1. package/AUTHORING.md +240 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +26 -10
  4. package/SUBMISSION.md +11 -12
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +36 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +2177 -353
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  20. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  21. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  22. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  23. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  24. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  25. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  26. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  27. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  28. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  29. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  30. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  31. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  32. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  33. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  34. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  35. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  36. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  37. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  38. package/dist/auth-turn/index.d.ts +195 -0
  39. package/dist/auth-turn/index.js +133 -0
  40. package/dist/auth.d.ts +76 -0
  41. package/dist/auth.js +427 -0
  42. package/dist/ceremonies/index.d.ts +1 -1
  43. package/dist/ceremonies/index.js +14 -48
  44. package/dist/cli/commands.d.ts +1 -1
  45. package/dist/cli/commands.js +8 -0
  46. package/dist/cli/create.d.ts +3 -0
  47. package/dist/cli/create.js +47 -33
  48. package/dist/cli/prompt-assets.d.ts +80 -0
  49. package/dist/cli/prompt-assets.js +743 -0
  50. package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  51. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  52. package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  53. package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  54. package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  55. package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  56. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  57. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  58. package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
  59. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  60. package/dist/cli/templates/provider/README.md.tpl +163 -0
  61. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  62. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  63. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  64. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  65. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  66. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  67. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  68. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  69. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  70. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  71. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  72. package/dist/config/loader.d.ts +149 -8
  73. package/dist/config/loader.js +378 -89
  74. package/dist/contract-serialization.d.ts +2 -2
  75. package/dist/contract-serialization.js +3 -6
  76. package/dist/contract-types.d.ts +2 -2
  77. package/dist/contract.d.ts +3 -3
  78. package/dist/contract.js +5 -6
  79. package/dist/define.d.ts +13 -1
  80. package/dist/define.js +245 -178
  81. package/dist/dev.d.ts +1 -1
  82. package/dist/dev.js +1 -1
  83. package/dist/errors.d.ts +4 -1
  84. package/dist/errors.js +48 -0
  85. package/dist/i18n/catalog.d.ts +2 -2
  86. package/dist/i18n/catalog.js +4 -10
  87. package/dist/i18n/index.d.ts +2 -2
  88. package/dist/i18n/index.js +2 -2
  89. package/dist/i18n/keys.d.ts +2 -2
  90. package/dist/index.d.ts +44 -41
  91. package/dist/index.js +39 -36
  92. package/dist/lint.d.ts +1 -0
  93. package/dist/lint.js +35 -15
  94. package/dist/provider.d.ts +11 -9
  95. package/dist/provider.js +9 -8
  96. package/dist/public-schema-field-lint.d.ts +1 -1
  97. package/dist/recipes/gov-api.js +1 -1
  98. package/dist/runtime/auth-flow.d.ts +1 -1
  99. package/dist/runtime/auth-flow.js +4 -2
  100. package/dist/runtime/browser.d.ts +1 -1
  101. package/dist/runtime/browser.js +214 -25
  102. package/dist/runtime/cache.d.ts +1 -1
  103. package/dist/runtime/cache.js +4 -8
  104. package/dist/runtime/choice.d.ts +1 -1
  105. package/dist/runtime/choice.js +31 -35
  106. package/dist/runtime/credential.d.ts +1 -1
  107. package/dist/runtime/credential.js +1 -1
  108. package/dist/runtime/env.d.ts +1 -1
  109. package/dist/runtime/executor.d.ts +1 -1
  110. package/dist/runtime/executor.js +15 -3
  111. package/dist/runtime/http.d.ts +2 -2
  112. package/dist/runtime/http.js +160 -344
  113. package/dist/runtime/insights.d.ts +1 -1
  114. package/dist/runtime/insights.js +6 -13
  115. package/dist/runtime/instrumentation.d.ts +2 -2
  116. package/dist/runtime/instrumentation.js +56 -19
  117. package/dist/runtime/keyring.js +1 -1
  118. package/dist/runtime/namespace.js +1 -1
  119. package/dist/runtime/otlp.d.ts +1 -1
  120. package/dist/runtime/perf.d.ts +1 -1
  121. package/dist/runtime/provider.d.ts +1 -1
  122. package/dist/runtime/provider.js +1 -2
  123. package/dist/runtime/proxy-errors.d.ts +1 -1
  124. package/dist/runtime/proxy-errors.js +9 -7
  125. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  126. package/dist/runtime/proxy-nodemaven.js +128 -0
  127. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  128. package/dist/runtime/proxy-retry-policy.js +326 -0
  129. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  130. package/dist/runtime/proxy-telemetry.js +55 -52
  131. package/dist/runtime/redis.d.ts +1 -1
  132. package/dist/runtime/redis.js +2 -4
  133. package/dist/runtime/request-options.d.ts +1 -1
  134. package/dist/runtime/secrets.d.ts +27 -0
  135. package/dist/runtime/secrets.js +51 -0
  136. package/dist/runtime/state.d.ts +2 -2
  137. package/dist/runtime/state.js +15 -4
  138. package/dist/runtime/stealth.d.ts +7 -4
  139. package/dist/runtime/stealth.js +257 -215
  140. package/dist/runtime/stt.d.ts +1 -1
  141. package/dist/runtime/stt.js +11 -15
  142. package/dist/runtime/trace.d.ts +2 -2
  143. package/dist/runtime/trace.js +2 -4
  144. package/dist/runtime/waterfall.d.ts +1 -1
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +7 -15
  147. package/dist/serve.d.ts +1 -1
  148. package/dist/serve.js +1 -1
  149. package/dist/server/index.d.ts +7 -3
  150. package/dist/server/index.js +6 -2
  151. package/dist/server/self-test-input-tokens.d.ts +1 -0
  152. package/dist/server/self-test-input-tokens.js +37 -0
  153. package/dist/server/self-test-redaction.d.ts +20 -0
  154. package/dist/server/self-test-redaction.js +70 -0
  155. package/dist/server/self-test-token.d.ts +30 -0
  156. package/dist/server/self-test-token.js +50 -0
  157. package/dist/server/self-test.d.ts +199 -0
  158. package/dist/server/self-test.js +1113 -0
  159. package/dist/server/serve.d.ts +14 -3
  160. package/dist/server/serve.js +135 -64
  161. package/dist/server/types.d.ts +10 -9
  162. package/dist/server/types.js +3 -7
  163. package/dist/stealth/profiles.d.ts +1 -1
  164. package/dist/stealth/profiles.js +5 -14
  165. package/dist/stream.d.ts +1 -1
  166. package/dist/testing/index.d.ts +2 -2
  167. package/dist/testing/index.js +2 -2
  168. package/dist/testing/run.d.ts +1 -1
  169. package/dist/testing/run.js +12 -15
  170. package/dist/types.d.ts +237 -1
  171. package/dist/user-input.d.ts +30 -0
  172. package/dist/user-input.js +66 -0
  173. package/package.json +16 -5
  174. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  175. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  176. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  177. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  178. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  179. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  180. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  181. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  182. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  183. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  184. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  185. package/src/auth-turn/fixtures/valid/form.json +14 -0
  186. package/src/auth-turn/fixtures/valid/message.json +13 -0
  187. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  188. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  189. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  190. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  191. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  192. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  193. package/src/auth-turn/index.ts +177 -0
  194. package/src/auth.ts +728 -0
  195. package/src/ceremonies/index.ts +33 -121
  196. package/src/cli/commands.ts +10 -0
  197. package/src/cli/create.ts +69 -99
  198. package/src/cli/prompt-assets.ts +865 -0
  199. package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  200. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  201. package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  202. package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  203. package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  204. package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  205. package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
  206. package/src/cli/templates/provider/README.md.tpl +7 -4
  207. package/src/config/loader.ts +543 -208
  208. package/src/contract-serialization.ts +5 -11
  209. package/src/contract-types.ts +2 -2
  210. package/src/contract.ts +13 -28
  211. package/src/define.ts +397 -528
  212. package/src/dev.ts +4 -9
  213. package/src/errors.ts +58 -5
  214. package/src/i18n/catalog.ts +10 -32
  215. package/src/i18n/index.ts +2 -2
  216. package/src/i18n/keys.ts +5 -11
  217. package/src/index.ts +64 -41
  218. package/src/lint.ts +122 -159
  219. package/src/provider.ts +40 -9
  220. package/src/public-schema-field-lint.ts +7 -33
  221. package/src/recipes/gov-api.ts +2 -5
  222. package/src/runtime/auth-flow.ts +6 -6
  223. package/src/runtime/browser.ts +320 -151
  224. package/src/runtime/cache.ts +20 -67
  225. package/src/runtime/choice.ts +79 -132
  226. package/src/runtime/credential.ts +2 -2
  227. package/src/runtime/env.ts +1 -1
  228. package/src/runtime/executor.ts +23 -20
  229. package/src/runtime/http.ts +216 -539
  230. package/src/runtime/insights.ts +15 -53
  231. package/src/runtime/instrumentation.ts +78 -65
  232. package/src/runtime/keyring.ts +7 -19
  233. package/src/runtime/namespace.ts +2 -7
  234. package/src/runtime/otlp.ts +12 -23
  235. package/src/runtime/perf.ts +1 -1
  236. package/src/runtime/provider.ts +4 -9
  237. package/src/runtime/proxy-errors.ts +29 -42
  238. package/src/runtime/proxy-nodemaven.ts +178 -0
  239. package/src/runtime/proxy-retry-policy.ts +469 -0
  240. package/src/runtime/proxy-telemetry.ts +79 -77
  241. package/src/runtime/redis.ts +4 -12
  242. package/src/runtime/request-options.ts +4 -13
  243. package/src/runtime/secrets.ts +64 -0
  244. package/src/runtime/state.ts +41 -110
  245. package/src/runtime/stealth.ts +331 -369
  246. package/src/runtime/stt.ts +38 -94
  247. package/src/runtime/trace.ts +14 -44
  248. package/src/runtime/waterfall.ts +5 -18
  249. package/src/schema.ts +23 -84
  250. package/src/serve.ts +1 -1
  251. package/src/server/index.ts +44 -3
  252. package/src/server/self-test-input-tokens.ts +46 -0
  253. package/src/server/self-test-redaction.ts +97 -0
  254. package/src/server/self-test-token.ts +70 -0
  255. package/src/server/self-test.ts +1450 -0
  256. package/src/server/serve.ts +206 -216
  257. package/src/server/types.ts +7 -19
  258. package/src/stealth/profiles.ts +10 -26
  259. package/src/stream.ts +8 -19
  260. package/src/testing/index.ts +2 -2
  261. package/src/testing/run.ts +24 -64
  262. package/src/types.ts +274 -1
  263. package/src/user-input.ts +118 -0
@@ -2,26 +2,28 @@ import { createHash } from "node:crypto";
2
2
  import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
3
3
  import { Impit } from "impit";
4
4
 
5
- import type { ProxyResolutionOptions } from "../config/loader";
5
+ import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
6
6
  import {
7
7
  DEFAULT_SMARTPROXY_POOL_SIZE,
8
8
  invalidateProxyResolutionCacheAsync,
9
+ policyResolvesRegistryVendorChain,
9
10
  ProxyResolutionError,
11
+ resolvePolicyProxyPoolSpan,
12
+ resolvePolicyTransportAttemptCap,
10
13
  resolveProxyConfigAsync,
11
- SMARTPROXY_MAX_POOL_SIZE,
12
- } from "../config/loader";
13
- import { ProviderError, SDKError, TransportError } from "../errors";
14
- import { getStealthProfile } from "../stealth/profiles";
14
+ vendorFromResolvedSource,
15
+ } from "../config/loader.js";
16
+ import { SDKError, TransportError } from "../errors.js";
17
+ import { getStealthProfile } from "../stealth/profiles.js";
15
18
  import type {
16
19
  CookieJar,
17
20
  HttpMethod,
18
- HttpRetryOptions,
19
21
  StealthClient,
20
22
  StealthFetchOptions,
23
+ StealthRedirectHop,
21
24
  StealthResponse,
22
25
  StealthSession,
23
- } from "../types";
24
- import { HttpRetryPreset, HttpRetryUnsafeMethodPolicy } from "../types";
26
+ } from "../types.js";
25
27
  import {
26
28
  createProxyAuthIpDeniedError,
27
29
  createProxyEdgeAuthRejectedError,
@@ -34,49 +36,31 @@ import {
34
36
  isProxyPoolRefreshableError,
35
37
  isProxyPoolStaleMessage,
36
38
  isProxyPoolStaleStatus,
37
- PROXY_AUTH_IP_DENIED_CODE,
38
39
  PROXY_EDGE_AUTH_REJECTED_CODE,
39
40
  PROXY_POOL_STALE_CODE,
40
- } from "./proxy-errors";
41
- import { appendQueryParams } from "./request-options";
41
+ } from "./proxy-errors.js";
42
+ import {
43
+ computeProxyAttemptIndex,
44
+ computeProxyTransportRetryDelayMs,
45
+ createDefaultProxyTransportRetryOptions,
46
+ normalizeProxyTransportRetryOptions,
47
+ shouldRetryProxyTransportAttempt,
48
+ validateUnsafeProxyTransportRetryMethods,
49
+ } from "./proxy-retry-policy.js";
50
+ import { appendQueryParams } from "./request-options.js";
42
51
 
43
52
  const DEFAULT_PROFILE = "chrome-146";
44
53
 
45
54
  const MISSING_PROXY_WARNING =
46
55
  "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
47
56
 
48
- const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
49
57
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
50
58
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
51
59
  const PROXY_CONNECT_FAILURE_BODY_PATTERN =
52
60
  /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
53
61
  const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
54
62
  const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
55
- const DEFAULT_STEALTH_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"] as const;
56
- const DEFAULT_STEALTH_RETRY_ERROR_CODES = [
57
- PROXY_CONNECT_FAILURE_CODE,
58
- "transport_network_error",
59
- "transport_timeout",
60
- ] as const;
61
- const RATE_LIMIT_STEALTH_RETRY_ERROR_CODES = ["transport_timeout"] as const;
62
- const KNOWN_STEALTH_RETRY_METHODS = new Set([
63
- "GET",
64
- "HEAD",
65
- "POST",
66
- "PUT",
67
- "DELETE",
68
- "OPTIONS",
69
- "TRACE",
70
- "PATCH",
71
- ]);
72
- const UNSAFE_STEALTH_RETRY_METHODS = new Set([
73
- "POST",
74
- "PUT",
75
- "PATCH",
76
- "DELETE",
77
- "TRACE",
78
- ]);
79
- const MAX_STEALTH_RETRY_ATTEMPTS = 8;
63
+ const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE] as const;
80
64
 
81
65
  export type StealthClientOptions = ProxyResolutionOptions & {
82
66
  warn?: (message: string) => void;
@@ -128,15 +112,15 @@ const FIREFOX_IMPIT_BY_MAJOR: Record<number, ImpitBrowser> = {
128
112
 
129
113
  type StealthTransportResponse = Pick<
130
114
  ImpitResponse,
131
- "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text" | "url"
132
- >;
115
+ "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
116
+ > & {
117
+ url?: string;
118
+ redirected?: boolean;
119
+ };
133
120
 
134
121
  type StealthMethod = NonNullable<ImpitRequestInit["method"]>;
135
- type NormalizedStealthRetryOptions = {
136
- attempts: number;
137
- methods: readonly string[];
138
- errorCodes: readonly string[];
139
- unsafeMethodPolicy: HttpRetryOptions["unsafeMethodPolicy"];
122
+ type StealthRequestInit = ImpitRequestInit & {
123
+ redirect?: NonNullable<StealthFetchOptions["redirect"]>;
140
124
  };
141
125
 
142
126
  function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
@@ -177,12 +161,37 @@ class CookieJarImpl implements CookieJar {
177
161
  return { ...this.cookies };
178
162
  }
179
163
 
164
+ has(name: string): boolean {
165
+ return Object.hasOwn(this.cookies, name);
166
+ }
167
+
180
168
  toString(): string {
181
169
  return Object.entries(this.cookies)
182
170
  .map(([name, value]) => `${name}=${value}`)
183
171
  .join("; ");
184
172
  }
185
173
 
174
+ toHeader(): string {
175
+ return this.toString();
176
+ }
177
+
178
+ snapshot(): Record<string, string> {
179
+ return this.getAll();
180
+ }
181
+
182
+ restore(cookies: Record<string, string>): void {
183
+ this.clear();
184
+ for (const [name, value] of Object.entries(cookies)) {
185
+ if (name) this.cookies[name] = value;
186
+ }
187
+ }
188
+
189
+ clear(): void {
190
+ for (const name of Object.keys(this.cookies)) {
191
+ delete this.cookies[name];
192
+ }
193
+ }
194
+
186
195
  find(predicate: (cookie: string) => boolean): string | undefined {
187
196
  for (const [name, value] of Object.entries(this.cookies)) {
188
197
  const cookie = `${name}=${value}`;
@@ -271,9 +280,7 @@ function normalizeHeaders(
271
280
  function hasOwn(object: object, key: string): boolean {
272
281
  return Object.hasOwn(object, key);
273
282
  }
274
- function toImpitCookieJar(
275
- cookieJar: CookieJarImpl,
276
- ): NonNullable<ImpitOptions["cookieJar"]> {
283
+ function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
277
284
  return {
278
285
  setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
279
286
  cookieJar.setFromCookieStrings([cookie]);
@@ -290,10 +297,8 @@ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
290
297
  const unsupported: string[] = [];
291
298
  if (hasOwn(options, "headerOrder")) unsupported.push("headerOrder");
292
299
  const stealth = options.stealth;
293
- if (isRecord(stealth) && hasOwn(stealth, "ja3"))
294
- unsupported.push("stealth.ja3");
295
- if (isRecord(stealth) && hasOwn(stealth, "h2"))
296
- unsupported.push("stealth.h2");
300
+ if (isRecord(stealth) && hasOwn(stealth, "ja3")) unsupported.push("stealth.ja3");
301
+ if (isRecord(stealth) && hasOwn(stealth, "h2")) unsupported.push("stealth.h2");
297
302
  if (unsupported.length === 0) return;
298
303
 
299
304
  throw new SDKError(
@@ -301,9 +306,7 @@ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
301
306
  );
302
307
  }
303
308
 
304
- function responseHeadersToRecord(
305
- headers: Headers,
306
- ): Record<string, string | string[] | undefined> {
309
+ function responseHeadersToRecord(headers: Headers): Record<string, string | string[] | undefined> {
307
310
  const record: Record<string, string> = {};
308
311
  for (const [name, value] of headers.entries()) record[name] = value;
309
312
  return record;
@@ -335,17 +338,22 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
335
338
 
336
339
  export async function normalizeResponse(
337
340
  response: StealthTransportResponse,
341
+ requestUrl?: string,
338
342
  ): Promise<StealthResponse> {
339
343
  const headers = Object.fromEntries(response.headers.entries());
340
- const cookies = new CookieJarImpl(
341
- setCookieHeadersFromResponse(response.headers),
342
- );
344
+ const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
343
345
  const bodyBytes = await response.arrayBuffer();
344
346
  const body = new TextDecoder().decode(bodyBytes);
345
347
 
346
348
  return {
347
349
  status: response.status,
348
350
  ok: response.status >= 200 && response.status < 300,
351
+ ...(response.url ? { url: response.url } : {}),
352
+ ...(response.redirected !== undefined
353
+ ? { redirected: response.redirected }
354
+ : requestUrl && response.url
355
+ ? { redirected: response.url !== requestUrl }
356
+ : {}),
349
357
  headers,
350
358
  rawHeaders: headerEntriesFromHeaders(response.headers),
351
359
  body,
@@ -383,189 +391,11 @@ function isPolicyManagedProxy(options: StealthClientOptions): boolean {
383
391
  return Boolean(policy && typeof policy === "object");
384
392
  }
385
393
 
386
- function isRetrySafeStealthMethod(method: StealthMethod): boolean {
387
- return method === "GET" || method === "HEAD" || method === "OPTIONS";
388
- }
389
-
390
- function createStealthRetryOptions(
391
- preset: HttpRetryPreset,
392
- ): NormalizedStealthRetryOptions {
393
- switch (preset) {
394
- case HttpRetryPreset.Off:
395
- return {
396
- attempts: 1,
397
- methods: DEFAULT_STEALTH_RETRY_METHODS,
398
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
399
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
400
- };
401
- case HttpRetryPreset.AggressiveRead:
402
- return {
403
- attempts: 4,
404
- methods: DEFAULT_STEALTH_RETRY_METHODS,
405
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
406
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
407
- };
408
- case HttpRetryPreset.RateLimitAware:
409
- return {
410
- attempts: 3,
411
- methods: DEFAULT_STEALTH_RETRY_METHODS,
412
- errorCodes: RATE_LIMIT_STEALTH_RETRY_ERROR_CODES,
413
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
414
- };
415
- case HttpRetryPreset.SafeRead:
416
- case HttpRetryPreset.TransportTransient:
417
- return {
418
- attempts: 3,
419
- methods: DEFAULT_STEALTH_RETRY_METHODS,
420
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
421
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
422
- };
423
- }
424
- throw new ProviderError(`Unknown stealth retry preset: ${preset}`, {
425
- code: "retry_invalid_policy",
426
- });
427
- }
428
-
429
- function normalizeStealthRetryOptions(
430
- retry: StealthFetchOptions["retry"],
431
- ): NormalizedStealthRetryOptions | undefined {
432
- if (retry === undefined) return undefined;
433
- if (retry === false) return createStealthRetryOptions(HttpRetryPreset.Off);
434
- if (retry === true)
435
- return createStealthRetryOptions(HttpRetryPreset.TransportTransient);
436
- if (typeof retry === "string") {
437
- if (!Object.values(HttpRetryPreset).includes(retry)) {
438
- throw new ProviderError(`Unknown stealth retry preset: ${retry}`, {
439
- code: "retry_invalid_policy",
440
- });
441
- }
442
- return createStealthRetryOptions(retry);
443
- }
444
- if (typeof retry !== "object" || retry === null || Array.isArray(retry)) {
445
- throw new ProviderError("Stealth retry policy must be a plain object", {
446
- code: "retry_invalid_policy",
447
- });
448
- }
449
- if (
450
- retry.unsafeMethodPolicy !== undefined &&
451
- !Object.values(HttpRetryUnsafeMethodPolicy).includes(
452
- retry.unsafeMethodPolicy,
453
- )
454
- ) {
455
- throw new ProviderError(
456
- `Unknown stealth retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
457
- { code: "retry_invalid_policy" },
458
- );
459
- }
460
- if (retry.methods !== undefined) {
461
- if (!Array.isArray(retry.methods)) {
462
- throw new ProviderError("Stealth retry methods must be an array", {
463
- code: "retry_invalid_policy",
464
- });
465
- }
466
- const unknownMethods = retry.methods
467
- .map((method) => (typeof method === "string" ? method.toUpperCase() : ""))
468
- .filter((method) => !KNOWN_STEALTH_RETRY_METHODS.has(method));
469
- if (unknownMethods.length > 0) {
470
- throw new ProviderError(
471
- `Unknown stealth retry method(s): ${unknownMethods.join(", ")}`,
472
- { code: "retry_invalid_policy" },
473
- );
474
- }
475
- }
476
- if (retry.errorCodes !== undefined) {
477
- if (
478
- !Array.isArray(retry.errorCodes) ||
479
- retry.errorCodes.some((errorCode) => typeof errorCode !== "string")
480
- ) {
481
- throw new ProviderError(
482
- "Stealth retry errorCodes must contain only strings",
483
- { code: "retry_invalid_policy" },
484
- );
485
- }
486
- }
487
-
488
- const base = createStealthRetryOptions(
489
- retry.preset ?? HttpRetryPreset.TransportTransient,
490
- );
491
- const attempts =
492
- retry.attempts === undefined || !Number.isFinite(retry.attempts)
493
- ? base.attempts
494
- : Math.max(
495
- 1,
496
- Math.min(MAX_STEALTH_RETRY_ATTEMPTS, Math.floor(retry.attempts)),
497
- );
498
- const normalized: NormalizedStealthRetryOptions = {
499
- attempts,
500
- methods:
501
- retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
502
- errorCodes: retry.errorCodes ?? base.errorCodes,
503
- unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
504
- };
505
-
506
- if (
507
- normalized.unsafeMethodPolicy !==
508
- HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
509
- ) {
510
- const unsafeMethods = normalized.methods.filter((method) =>
511
- UNSAFE_STEALTH_RETRY_METHODS.has(method.toUpperCase()),
512
- );
513
- if (unsafeMethods.length > 0) {
514
- throw new ProviderError(
515
- `Stealth retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
516
- { code: "retry_unsafe_method" },
517
- );
518
- }
519
- }
520
-
521
- return normalized;
522
- }
523
-
524
- function isExplicitStealthRetryAllowed(
525
- method: StealthMethod,
526
- error: TransportError,
527
- retryOptions: NormalizedStealthRetryOptions | undefined,
528
- ): boolean {
529
- if (!retryOptions || retryOptions.attempts <= 1) return false;
530
- return (
531
- retryOptions.methods.includes(method.toUpperCase()) &&
532
- retryOptions.errorCodes.includes(proxyAttemptErrorCode(error))
533
- );
534
- }
535
-
536
- function isRetryableProxyTransportError(error: unknown): boolean {
537
- if (error instanceof TransportError) {
538
- if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
539
- return false;
540
- }
541
- return (
542
- error.code === PROXY_CONNECT_FAILURE_CODE ||
543
- error.code === "transport_network_error" ||
544
- error.code === "transport_timeout"
545
- );
546
- }
547
-
548
- if (error instanceof SDKError) {
549
- return false;
550
- }
551
-
552
- const message = error instanceof Error ? error.message : String(error);
553
- return /\bproxy\b|\bnon[\s-]?200\b|\bconnect\b|\btunnel\b/i.test(message);
554
- }
555
-
556
- function isProxyConnectFailureResponse(
557
- response: StealthTransportResponse,
558
- body: string,
559
- ): boolean {
560
- return (
561
- response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "")
562
- );
394
+ function isProxyConnectFailureResponse(response: StealthTransportResponse, body: string): boolean {
395
+ return response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "");
563
396
  }
564
397
 
565
- function createProxyConnectFailureError(
566
- body: string,
567
- cause?: Error,
568
- ): TransportError {
398
+ function createProxyConnectFailureError(body: string, cause?: Error): TransportError {
569
399
  const bodyExcerpt = (body ?? "").trim().slice(0, 1_000);
570
400
  if (isProxyAuthIpDeniedMessage(bodyExcerpt)) {
571
401
  return createProxyAuthIpDeniedError(cause);
@@ -574,10 +404,7 @@ function createProxyConnectFailureError(
574
404
  return createProxyEdgeAuthRejectedError(cause);
575
405
  }
576
406
  if (isProxyPoolStaleMessage(bodyExcerpt)) {
577
- return createProxyPoolStaleError(
578
- bodyExcerpt.includes("512") ? 512 : 509,
579
- cause,
580
- );
407
+ return createProxyPoolStaleError(bodyExcerpt.includes("512") ? 512 : 509, cause);
581
408
  }
582
409
  return new TransportError(bodyExcerpt || "Proxy CONNECT failed", {
583
410
  code: PROXY_CONNECT_FAILURE_CODE,
@@ -601,6 +428,7 @@ type ResolvedAttemptProxy = {
601
428
  url?: string;
602
429
  poolIndex?: number;
603
430
  proxyHash?: string;
431
+ vendor?: ProxyVendorName;
604
432
  };
605
433
 
606
434
  function proxyPoolIndexFromDiagnostics(
@@ -684,33 +512,20 @@ function normalizeStealthTransportError(error: unknown): TransportError {
684
512
  }
685
513
 
686
514
  if (isProxyAuthIpDeniedMessage(message)) {
687
- return createProxyAuthIpDeniedError(
688
- error instanceof Error ? error : undefined,
689
- );
515
+ return createProxyAuthIpDeniedError(error instanceof Error ? error : undefined);
690
516
  }
691
517
 
692
518
  if (isProxyEdgeAuthRejectedMessage(message)) {
693
- return createProxyEdgeAuthRejectedError(
694
- error instanceof Error ? error : undefined,
695
- );
519
+ return createProxyEdgeAuthRejectedError(error instanceof Error ? error : undefined);
696
520
  }
697
521
 
698
522
  const proxyTunnelStatus = getProxyTunnelStatus(error);
699
- if (
700
- proxyTunnelStatus !== undefined &&
701
- isProxyPoolStaleStatus(proxyTunnelStatus)
702
- ) {
703
- return createProxyPoolStaleError(
704
- proxyTunnelStatus,
705
- error instanceof Error ? error : undefined,
706
- );
523
+ if (proxyTunnelStatus !== undefined && isProxyPoolStaleStatus(proxyTunnelStatus)) {
524
+ return createProxyPoolStaleError(proxyTunnelStatus, error instanceof Error ? error : undefined);
707
525
  }
708
526
 
709
527
  if (PROXY_CONNECT_FAILURE_BODY_PATTERN.test(message)) {
710
- return createProxyConnectFailureError(
711
- message,
712
- error instanceof Error ? error : undefined,
713
- );
528
+ return createProxyConnectFailureError(message, error instanceof Error ? error : undefined);
714
529
  }
715
530
 
716
531
  return new TransportError("Network error", {
@@ -720,6 +535,10 @@ function normalizeStealthTransportError(error: unknown): TransportError {
720
535
  });
721
536
  }
722
537
 
538
+ function sleep(ms: number): Promise<void> {
539
+ return new Promise((resolve) => setTimeout(resolve, ms));
540
+ }
541
+
723
542
  function normalizeMethod(method: HttpMethod | string): StealthMethod {
724
543
  switch (method.toUpperCase()) {
725
544
  case "HEAD":
@@ -743,6 +562,23 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
743
562
  }
744
563
  }
745
564
 
565
+ function isRedirectStatus(status: number): boolean {
566
+ return [301, 302, 303, 307, 308].includes(status);
567
+ }
568
+
569
+ function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
570
+ if (status === 303 && method !== "HEAD") return "GET";
571
+ if ((status === 301 || status === 302) && method === "POST") return "GET";
572
+ return method;
573
+ }
574
+
575
+ function locationHeader(headers: Record<string, string>): string | undefined {
576
+ for (const [name, value] of Object.entries(headers)) {
577
+ if (name.toLowerCase() === "location") return value;
578
+ }
579
+ return undefined;
580
+ }
581
+
746
582
  function createSessionFetcher(
747
583
  baseUrl: string,
748
584
  defaultProfile: string,
@@ -782,20 +618,22 @@ function createSessionFetcher(
782
618
  async function resolveRequestProxy(
783
619
  options?: StealthFetchOptions,
784
620
  proxyAttempt?: number,
621
+ refreshEpoch?: number,
785
622
  ): Promise<ResolvedAttemptProxy> {
786
- const rawProxyAttemptOffset = options?.proxyAttemptOffset ?? 0;
787
- const proxyAttemptOffset = Number.isFinite(rawProxyAttemptOffset)
788
- ? Math.max(0, Math.floor(rawProxyAttemptOffset))
789
- : 0;
790
623
  const resolvedProxy = await resolveProxyConfigAsync({
791
624
  proxy: options?.proxy ?? clientOptions.proxy,
792
625
  upstream: clientOptions.upstream,
793
626
  apifuseConfig: clientOptions.apifuseConfig,
794
627
  affinityKey: clientOptions.affinityKey,
795
- proxyAttempt:
796
- proxyAttempt === undefined
797
- ? proxyAttemptOffset
798
- : proxyAttemptOffset + proxyAttempt,
628
+ proxyAttempt: computeProxyAttemptIndex({
629
+ baseProxyAttempt: clientOptions.proxyAttempt,
630
+ proxyAttemptOffset: options?.proxyAttemptOffset,
631
+ retryAttemptOffset: proxyAttempt,
632
+ }),
633
+ // The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
634
+ // preserving the client TLS fingerprint end-to-end.
635
+ transportProtocols: ["http", "socks5"],
636
+ ...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
799
637
  telemetry: clientOptions.telemetry,
800
638
  });
801
639
 
@@ -808,30 +646,58 @@ function createSessionFetcher(
808
646
  url: resolvedProxy.url,
809
647
  poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
810
648
  proxyHash: proxyEndpointHash(resolvedProxy.url),
649
+ vendor: vendorFromResolvedSource(resolvedProxy.source),
811
650
  };
812
651
  }
813
652
 
814
- return {
653
+ const session: StealthSession = {
815
654
  async fetch(url, options: StealthFetchOptions = {}) {
816
655
  const method = normalizeMethod(options.method ?? "GET");
817
656
  const hasExplicitRetryPolicy = options.retry !== undefined;
818
- const stealthRetryOptions = normalizeStealthRetryOptions(options.retry);
657
+ const stealthRetryOptions =
658
+ normalizeProxyTransportRetryOptions(options.retry, {
659
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
660
+ label: "Stealth",
661
+ }) ??
662
+ (hasExplicitRetryPolicy
663
+ ? undefined
664
+ : createDefaultProxyTransportRetryOptions({
665
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
666
+ label: "Stealth",
667
+ }));
668
+ if (stealthRetryOptions) {
669
+ validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
670
+ }
819
671
  const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
820
- const usesPolicyAllocator =
821
- hasPolicyProxy && !options.proxy && !clientOptions.proxy;
822
- const maxAttempts = usesPolicyAllocator
823
- ? Math.max(
824
- 1,
825
- Math.min(
826
- MAX_POLICY_PROXY_RETRY_ATTEMPTS,
827
- clientOptions.proxyPolicy?.session?.poolSize ??
828
- (typeof clientOptions.upstream?.proxy === "object"
829
- ? clientOptions.upstream.proxy.session?.poolSize
830
- : undefined) ??
831
- DEFAULT_SMARTPROXY_POOL_SIZE,
832
- ),
833
- )
834
- : 1;
672
+ const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
673
+ const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
674
+ // Span the whole vendor chain: successive attempts rotate one vendor's
675
+ // pool, then fail over to the next vendor via the flat attempt index.
676
+ const policyProxy =
677
+ clientOptions.proxyPolicy ??
678
+ (typeof clientOptions.upstream?.proxy === "object"
679
+ ? clientOptions.upstream.proxy
680
+ : undefined);
681
+ // The pool span is already bounded by each vendor's max pool size
682
+ // (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
683
+ // the chain's true maximum — a large NodeMaven pool stays fully
684
+ // reachable rather than being truncated at an arbitrary ceiling.
685
+ const policyProxyAttemptCap = Math.max(
686
+ 1,
687
+ policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
688
+ );
689
+ // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
690
+ // successive attempts resolve a *different* endpoint, so it is the only one
691
+ // that may widen the attempt cap to the pool span, de-duplicate endpoints,
692
+ // and drive allocator stale-pool refresh. A static custom/decodo policy
693
+ // resolves the same URL every attempt: widening/refreshing it would resend
694
+ // the request dozens of times (up to maxAttempts × refreshes) and bypass
695
+ // retry:false and unsafe-method controls. Static policies therefore follow
696
+ // the ordinary transport-retry budget instead.
697
+ const rotatesRegistryChain =
698
+ usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
699
+ const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
700
+ const dedupeAllocatorEndpoints = rotatesRegistryChain;
835
701
  let lastError: unknown;
836
702
 
837
703
  for (
@@ -856,14 +722,12 @@ function createSessionFetcher(
856
722
  if (attemptRecorded || !proxy) return;
857
723
  attemptRecorded = true;
858
724
  clientOptions.telemetry?.recordProxyAttempt?.({
859
- provider: "smartproxy",
725
+ provider: attemptProxy?.vendor ?? "smartproxy",
860
726
  attempt: attempt + 1,
861
727
  ...(attemptProxy?.poolIndex === undefined
862
728
  ? {}
863
729
  : { poolIndex: attemptProxy.poolIndex }),
864
- ...(attemptProxy?.proxyHash
865
- ? { proxyHash: attemptProxy.proxyHash }
866
- : {}),
730
+ ...(attemptProxy?.proxyHash ? { proxyHash: attemptProxy.proxyHash } : {}),
867
731
  outcome,
868
732
  ...(errorCode ? { errorCode } : {}),
869
733
  ...(status === undefined ? {} : { status }),
@@ -872,52 +736,47 @@ function createSessionFetcher(
872
736
  };
873
737
  try {
874
738
  assertNoUnsupportedFingerprintOverrides(options);
875
- attemptProxy = await resolveRequestProxy(options, attempt);
739
+ attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
876
740
  proxy = attemptProxy.url;
877
- if (proxy) {
741
+ if (proxy && dedupeAllocatorEndpoints) {
742
+ // An under-filled allocation repeats endpoints (via the modulo
743
+ // pool mapping) before the flat offset crosses into the next
744
+ // vendor. Skip an already-tried endpoint and advance the offset
745
+ // rather than breaking — breaking here would strand the request on
746
+ // the primary vendor and never reach the fallback leg.
878
747
  if (attemptedProxies.has(proxy)) {
879
- break;
748
+ continue;
880
749
  }
881
750
  attemptedProxies.add(proxy);
882
751
  }
883
752
  const ignoreTlsErrors = Boolean(
884
753
  options.stealth?.insecureSkipVerify ??
885
- (!hasPolicyProxy &&
886
- proxy &&
887
- clientOptions.proxyStealth?.insecureSkipVerify),
754
+ (!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify),
888
755
  );
889
756
  const profileName = options.profile ?? defaultProfile;
890
- const requestUrl = appendQueryParams(
891
- resolveUrl(baseUrl, url),
892
- options.params,
893
- );
757
+ const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
894
758
  const headers = { ...(options.headers ?? {}) };
895
759
  if (!hasHeader(headers, "Cookie")) {
896
760
  const cookieHeader = cookieJar.toString();
897
761
  if (cookieHeader) headers.Cookie = cookieHeader;
898
762
  }
899
- const requestInit: ImpitRequestInit = {
763
+ const requestInit: StealthRequestInit = {
900
764
  headers: normalizeHeaders(headers),
901
765
  method,
766
+ ...(options.redirect ? { redirect: options.redirect } : {}),
902
767
  ...(options.timeout ? { timeout: options.timeout } : {}),
903
768
  };
904
769
  if (options.body !== undefined) {
905
770
  requestInit.body = normalizeBody(options.body);
906
771
  }
907
- const response = await getClient(
908
- profileName,
909
- proxy,
910
- ignoreTlsErrors,
911
- ).fetch(requestUrl, requestInit);
912
- const normalized = await normalizeResponse(response);
913
- cookieJar.setFromCookieStrings(
914
- setCookieHeadersFromResponse(response.headers),
772
+ const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(
773
+ requestUrl,
774
+ requestInit,
915
775
  );
776
+ const normalized = await normalizeResponse(response, requestUrl);
777
+ cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
916
778
 
917
- if (
918
- proxy &&
919
- isProxyConnectFailureResponse(response, normalized.body)
920
- ) {
779
+ if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
921
780
  throw createProxyConnectFailureError(normalized.body);
922
781
  }
923
782
 
@@ -927,10 +786,9 @@ function createSessionFetcher(
927
786
  usesPolicyAllocator &&
928
787
  isProxyEdgeTlsRejectedResponse(
929
788
  response.status,
930
- [
931
- JSON.stringify(responseHeadersToRecord(response.headers)),
932
- normalized.body,
933
- ].join("\n"),
789
+ [JSON.stringify(responseHeadersToRecord(response.headers)), normalized.body].join(
790
+ "\n",
791
+ ),
934
792
  )
935
793
  ) {
936
794
  throw createProxyEdgeTlsRejectedError(response.status);
@@ -951,13 +809,10 @@ function createSessionFetcher(
951
809
  }
952
810
 
953
811
  if (response.status >= 400 && options.throwOnHttpError !== false) {
954
- throw new TransportError(
955
- `Upstream request failed with status ${response.status}`,
956
- {
957
- code: "upstream_http_error",
958
- status: response.status,
959
- },
960
- );
812
+ throw new TransportError(`Upstream request failed with status ${response.status}`, {
813
+ code: "upstream_http_error",
814
+ status: response.status,
815
+ });
961
816
  }
962
817
 
963
818
  recordProxyAttempt("ok", undefined, response.status);
@@ -970,11 +825,7 @@ function createSessionFetcher(
970
825
  proxyAttemptStatus(normalizedError),
971
826
  );
972
827
  lastError = normalizedError;
973
- if (
974
- proxy &&
975
- usesPolicyAllocator &&
976
- isProxyPoolRefreshableError(normalizedError)
977
- ) {
828
+ if (proxy && rotatesRegistryChain && isProxyPoolRefreshableError(normalizedError)) {
978
829
  stalePoolError = normalizedError;
979
830
  if (shouldRunProxyAuthDiagnostic(normalizedError)) {
980
831
  stalePoolDiagnosticProxy = proxy;
@@ -984,21 +835,39 @@ function createSessionFetcher(
984
835
  }
985
836
  break;
986
837
  }
838
+ // Cap the number of transport retries. For a policy-allocator chain,
839
+ // every attempt resolves a *different* endpoint/vendor (poolIndex
840
+ // rotates across the concatenated vendor pool spans), so a transport
841
+ // failure is a signal to advance to the next endpoint — potentially
842
+ // crossing into the fallback vendor — not to retry the same endpoint.
843
+ // Truncating that rotation at the per-endpoint retry budget would
844
+ // strand the request on the primary vendor and never reach the
845
+ // fallback, since the crossover only happens once the flat attempt
846
+ // index exceeds the primary vendor's pool size (~10-20).
847
+ // resolvePolicyTransportAttemptCap widens to the full chain span only
848
+ // for implicit, safe-method allocator requests; explicit retry
849
+ // policies (their documented `attempts` ceiling), unsafe methods, and
850
+ // static/non-registry vendors keep the per-endpoint retry budget.
851
+ const transportRetryCap = resolvePolicyTransportAttemptCap({
852
+ policy: policyProxy,
853
+ usesPolicyAllocator,
854
+ retryAttempts: stealthRetryOptions?.attempts ?? 1,
855
+ explicitRetry: hasExplicitRetryPolicy,
856
+ method,
857
+ });
987
858
  if (
988
- proxy &&
989
- attempt + 1 <
990
- (stealthRetryOptions
991
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
992
- : maxAttempts) &&
993
- (!hasExplicitRetryPolicy
994
- ? isRetrySafeStealthMethod(method)
995
- : isExplicitStealthRetryAllowed(
996
- method,
997
- normalizedError,
998
- stealthRetryOptions,
999
- )) &&
1000
- isRetryableProxyTransportError(normalizedError)
859
+ attempt + 1 < transportRetryCap &&
860
+ shouldRetryProxyTransportAttempt({
861
+ error: normalizedError,
862
+ explicitRetry: hasExplicitRetryPolicy,
863
+ method,
864
+ options: stealthRetryOptions,
865
+ proxyUsed: Boolean(proxy),
866
+ })
1001
867
  ) {
868
+ if (stealthRetryOptions) {
869
+ await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions!, attempt + 1));
870
+ }
1002
871
  continue;
1003
872
  }
1004
873
  throw normalizedError;
@@ -1006,7 +875,7 @@ function createSessionFetcher(
1006
875
  }
1007
876
 
1008
877
  if (
1009
- usesPolicyAllocator &&
878
+ rotatesRegistryChain &&
1010
879
  stalePoolError &&
1011
880
  refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
1012
881
  ) {
@@ -1052,33 +921,138 @@ function createSessionFetcher(
1052
921
 
1053
922
  throw normalizeStealthTransportError(lastError);
1054
923
  },
924
+ cookies: cookieJar,
925
+ redirects: {
926
+ async run(options) {
927
+ const maxHops =
928
+ options.maxHops === undefined || !Number.isFinite(options.maxHops)
929
+ ? 10
930
+ : Math.max(0, Math.floor(options.maxHops));
931
+ const hops: StealthRedirectHop[] = [];
932
+ let currentUrl = resolveUrl(baseUrl, options.url);
933
+ let method = normalizeMethod(options.method ?? "GET");
934
+ let body = options.body;
935
+ let response: StealthResponse | undefined;
936
+ const visitedRequests = new Set<string>();
937
+
938
+ const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
939
+
940
+ for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
941
+ visitedRequests.add(`${method} ${currentUrl}`);
942
+ response = await session.fetch(currentUrl, {
943
+ ...fetchOptions,
944
+ body,
945
+ method,
946
+ ...(hopIndex === 0 && params ? { params } : {}),
947
+ redirect: "manual",
948
+ throwOnHttpError: false,
949
+ });
950
+
951
+ if (!isRedirectStatus(response.status)) {
952
+ return {
953
+ final: response,
954
+ hops,
955
+ reason: "completed",
956
+ cookies: cookieJar.snapshot(),
957
+ };
958
+ }
959
+
960
+ const location = locationHeader(response.headers);
961
+ const nextUrl = location
962
+ ? new URL(location, response.url ?? currentUrl).toString()
963
+ : undefined;
964
+ const hop: StealthRedirectHop = {
965
+ url: response.url ?? currentUrl,
966
+ status: response.status,
967
+ method,
968
+ ...(location ? { location } : {}),
969
+ ...(nextUrl ? { nextUrl } : {}),
970
+ };
971
+ hops.push(hop);
972
+
973
+ if (stopWhen && (await stopWhen(hop))) {
974
+ return {
975
+ final: response,
976
+ hops,
977
+ reason: "stopped",
978
+ cookies: cookieJar.snapshot(),
979
+ };
980
+ }
981
+
982
+ if (!nextUrl) {
983
+ return {
984
+ final: response,
985
+ hops,
986
+ reason: "missing_location",
987
+ cookies: cookieJar.snapshot(),
988
+ };
989
+ }
990
+
991
+ if (hops.length > maxHops) {
992
+ return {
993
+ final: response,
994
+ hops,
995
+ reason: "max_hops",
996
+ cookies: cookieJar.snapshot(),
997
+ };
998
+ }
999
+
1000
+ const nextMethod = nextRedirectMethod(response.status, method);
1001
+ if (nextMethod !== method) {
1002
+ body = undefined;
1003
+ }
1004
+ if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
1005
+ return {
1006
+ final: response,
1007
+ hops,
1008
+ reason: "loop",
1009
+ cookies: cookieJar.snapshot(),
1010
+ };
1011
+ }
1012
+ method = nextMethod;
1013
+ currentUrl = nextUrl;
1014
+ }
1015
+
1016
+ if (!response) {
1017
+ response = await session.fetch(currentUrl, {
1018
+ ...fetchOptions,
1019
+ body,
1020
+ method,
1021
+ ...(params ? { params } : {}),
1022
+ redirect: "manual",
1023
+ throwOnHttpError: false,
1024
+ });
1025
+ }
1026
+ return {
1027
+ final: response,
1028
+ hops,
1029
+ reason: "max_hops",
1030
+ cookies: cookieJar.snapshot(),
1031
+ };
1032
+ },
1033
+ },
1055
1034
  close() {
1056
1035
  closed = true;
1057
1036
  clients.clear();
1058
1037
  },
1059
1038
  };
1039
+ return session;
1060
1040
 
1061
1041
  async function classifyProxyAuthDiagnostic(
1062
1042
  profileName: string,
1063
1043
  proxy: string,
1064
1044
  ): Promise<"source_ip_denied" | "edge_auth_rejected" | undefined> {
1065
1045
  try {
1066
- const response = await getClient(profileName, proxy, false).fetch(
1067
- PROXY_AUTH_DIAGNOSTIC_URL,
1068
- {
1069
- method: "GET",
1070
- timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
1071
- },
1072
- );
1046
+ const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
1047
+ method: "GET",
1048
+ timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
1049
+ });
1073
1050
  const normalized = await normalizeResponse(response);
1074
1051
  return classifyProxyAuthDiagnosticMessage(normalized.body);
1075
1052
  } catch (error) {
1076
1053
  const message =
1077
1054
  error instanceof Error
1078
- ? [
1079
- error.message,
1080
- error.cause instanceof Error ? error.cause.message : "",
1081
- ]
1055
+ ? [error.message, error.cause instanceof Error ? error.cause.message : ""]
1082
1056
  .filter(Boolean)
1083
1057
  .join(" ")
1084
1058
  : String(error);
@@ -1118,22 +1092,14 @@ export function createStealthClient(
1118
1092
  clientOptions: StealthClientOptions = {},
1119
1093
  ): StealthClient {
1120
1094
  const defaultProfile =
1121
- typeof defaultProfileOrOptions === "string"
1122
- ? defaultProfileOrOptions
1123
- : DEFAULT_PROFILE;
1095
+ typeof defaultProfileOrOptions === "string" ? defaultProfileOrOptions : DEFAULT_PROFILE;
1124
1096
  const resolvedClientOptions =
1125
- typeof defaultProfileOrOptions === "string"
1126
- ? clientOptions
1127
- : defaultProfileOrOptions;
1097
+ typeof defaultProfileOrOptions === "string" ? clientOptions : defaultProfileOrOptions;
1128
1098
  let sharedSession: StealthSession | null = null;
1129
1099
 
1130
1100
  function getSharedSession(): StealthSession {
1131
1101
  if (!sharedSession) {
1132
- sharedSession = createSessionFetcher(
1133
- baseUrl,
1134
- defaultProfile,
1135
- resolvedClientOptions,
1136
- );
1102
+ sharedSession = createSessionFetcher(baseUrl, defaultProfile, resolvedClientOptions);
1137
1103
  }
1138
1104
 
1139
1105
  return sharedSession;
@@ -1145,11 +1111,7 @@ export function createStealthClient(
1145
1111
  },
1146
1112
  createSession(opts?: { profile?: string }) {
1147
1113
  const sessionProfile = opts?.profile ?? defaultProfile;
1148
- return createSessionFetcher(
1149
- baseUrl,
1150
- sessionProfile,
1151
- resolvedClientOptions,
1152
- );
1114
+ return createSessionFetcher(baseUrl, sessionProfile, resolvedClientOptions);
1153
1115
  },
1154
1116
  close() {
1155
1117
  sharedSession?.close();