@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40

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 (303) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +165 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +88 -4
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-pack-check.ts +22 -2
  8. package/bin/apifuse-pack-smoke.ts +57 -2
  9. package/bin/apifuse-pack-types.ts +356 -38
  10. package/bin/apifuse-perf.ts +14 -13
  11. package/bin/apifuse-record.ts +691 -68
  12. package/bin/apifuse-submit-check.ts +518 -37
  13. package/bin/apifuse-sync-assets.ts +117 -0
  14. package/bin/submit-check-delimited-text.ts +50 -0
  15. package/dist/auth-turn/index.d.ts +3 -3
  16. package/dist/auth-turn/index.js +1 -1
  17. package/dist/auth.d.ts +14 -0
  18. package/dist/auth.js +67 -0
  19. package/dist/ceremonies/index.d.ts +16 -0
  20. package/dist/ceremonies/index.js +141 -36
  21. package/dist/cli/commands.d.ts +1 -1
  22. package/dist/cli/commands.js +8 -0
  23. package/dist/cli/create.d.ts +3 -0
  24. package/dist/cli/create.js +34 -35
  25. package/dist/cli/prompt-assets.d.ts +80 -0
  26. package/dist/cli/prompt-assets.js +743 -0
  27. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  29. package/dist/cli/templates/provider/README.md.tpl +4 -4
  30. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  31. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  32. package/dist/config/loader.d.ts +177 -16
  33. package/dist/config/loader.js +424 -127
  34. package/dist/contract-serialization.js +4 -8
  35. package/dist/contract-types.d.ts +1 -0
  36. package/dist/contract.js +2 -0
  37. package/dist/declaration-validation.d.ts +32 -0
  38. package/dist/declaration-validation.js +207 -0
  39. package/dist/define.d.ts +51 -25
  40. package/dist/define.js +752 -38
  41. package/dist/error-resolution.d.ts +4 -0
  42. package/dist/error-resolution.js +122 -0
  43. package/dist/errors.d.ts +18 -0
  44. package/dist/errors.js +40 -0
  45. package/dist/fixture-sanitization.d.ts +28 -0
  46. package/dist/fixture-sanitization.js +217 -0
  47. package/dist/health-scenario.d.ts +1842 -0
  48. package/dist/health-scenario.js +624 -0
  49. package/dist/index.d.ts +19 -9
  50. package/dist/index.js +10 -6
  51. package/dist/lint.d.ts +6 -1
  52. package/dist/lint.js +362 -3
  53. package/dist/native-address.d.ts +43 -0
  54. package/dist/native-address.js +281 -0
  55. package/dist/native-egress-policy.d.ts +31 -0
  56. package/dist/native-egress-policy.js +288 -0
  57. package/dist/observability.d.ts +5 -2
  58. package/dist/observability.js +48 -1
  59. package/dist/provider.d.ts +8 -2
  60. package/dist/provider.js +3 -1
  61. package/dist/runtime/auth-flow.d.ts +5 -1
  62. package/dist/runtime/auth-flow.js +6 -0
  63. package/dist/runtime/browser.d.ts +1 -0
  64. package/dist/runtime/browser.js +492 -49
  65. package/dist/runtime/cache.d.ts +1 -0
  66. package/dist/runtime/cache.js +169 -15
  67. package/dist/runtime/choice-wordlist.d.ts +9 -0
  68. package/dist/runtime/choice-wordlist.js +138 -0
  69. package/dist/runtime/choice.d.ts +13 -1
  70. package/dist/runtime/choice.js +490 -102
  71. package/dist/runtime/executor.d.ts +2 -2
  72. package/dist/runtime/executor.js +26 -2
  73. package/dist/runtime/http.d.ts +1 -0
  74. package/dist/runtime/http.js +515 -53
  75. package/dist/runtime/instrumentation.d.ts +2 -2
  76. package/dist/runtime/instrumentation.js +366 -8
  77. package/dist/runtime/native-network-errors.d.ts +33 -0
  78. package/dist/runtime/native-network-errors.js +69 -0
  79. package/dist/runtime/native-network.d.ts +96 -0
  80. package/dist/runtime/native-network.js +1232 -0
  81. package/dist/runtime/ocr.d.ts +29 -0
  82. package/dist/runtime/ocr.js +440 -0
  83. package/dist/runtime/proxy-errors.js +6 -2
  84. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  85. package/dist/runtime/proxy-nodemaven.js +146 -0
  86. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  87. package/dist/runtime/proxy-telemetry.js +154 -47
  88. package/dist/runtime/redirects.d.ts +29 -0
  89. package/dist/runtime/redirects.js +36 -0
  90. package/dist/runtime/redis.d.ts +1 -1
  91. package/dist/runtime/redis.js +4 -2
  92. package/dist/runtime/request-options.d.ts +68 -1
  93. package/dist/runtime/request-options.js +548 -0
  94. package/dist/runtime/resolver-config.d.ts +6 -0
  95. package/dist/runtime/resolver-config.js +6 -0
  96. package/dist/runtime/resolver-public.d.ts +1 -0
  97. package/dist/runtime/resolver-public.js +1 -0
  98. package/dist/runtime/resolver-shared.d.ts +3 -0
  99. package/dist/runtime/resolver-shared.js +12 -0
  100. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  101. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  102. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  103. package/dist/runtime/resolver-vendors/browser.js +377 -0
  104. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  105. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  106. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  107. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  108. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  109. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  110. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  111. package/dist/runtime/resolver-vendors/types.js +96 -0
  112. package/dist/runtime/resolver.d.ts +60 -0
  113. package/dist/runtime/resolver.js +737 -0
  114. package/dist/runtime/secrets.d.ts +27 -0
  115. package/dist/runtime/secrets.js +51 -0
  116. package/dist/runtime/state.d.ts +3 -0
  117. package/dist/runtime/state.js +277 -71
  118. package/dist/runtime/stealth-cookies.d.ts +20 -0
  119. package/dist/runtime/stealth-cookies.js +111 -0
  120. package/dist/runtime/stealth.d.ts +28 -3
  121. package/dist/runtime/stealth.js +519 -255
  122. package/dist/runtime/stt.js +1 -12
  123. package/dist/runtime/timeout.d.ts +5 -0
  124. package/dist/runtime/timeout.js +12 -0
  125. package/dist/runtime/trace-config.d.ts +12 -0
  126. package/dist/runtime/trace-config.js +61 -0
  127. package/dist/serve.d.ts +1 -1
  128. package/dist/serve.js +1 -1
  129. package/dist/server/index.d.ts +5 -3
  130. package/dist/server/index.js +3 -3
  131. package/dist/server/self-test-input-tokens.d.ts +2 -1
  132. package/dist/server/self-test-input-tokens.js +18 -14
  133. package/dist/server/self-test.d.ts +114 -0
  134. package/dist/server/self-test.js +784 -148
  135. package/dist/server/serve-implementation.d.ts +213 -0
  136. package/dist/server/serve-implementation.js +2173 -0
  137. package/dist/server/serve.d.ts +1 -70
  138. package/dist/server/serve.js +1 -1130
  139. package/dist/server/trace-output.d.ts +4 -0
  140. package/dist/server/trace-output.js +20 -0
  141. package/dist/server/types.d.ts +30 -5
  142. package/dist/server/types.js +13 -1
  143. package/dist/stateful/errors.d.ts +19 -0
  144. package/dist/stateful/errors.js +24 -0
  145. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  146. package/dist/stateful/http-provider-event-emitter.js +237 -0
  147. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  148. package/dist/stateful/http-session-owner-registry.js +210 -0
  149. package/dist/stateful/index.d.ts +18 -0
  150. package/dist/stateful/index.js +18 -0
  151. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  152. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  153. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  154. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  155. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  156. package/dist/stateful/provider-event-pipeline.js +1 -0
  157. package/dist/stateful/provider-events.d.ts +101 -0
  158. package/dist/stateful/provider-events.js +289 -0
  159. package/dist/stateful/session-key.d.ts +15 -0
  160. package/dist/stateful/session-key.js +86 -0
  161. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  162. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  163. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  164. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  165. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  166. package/dist/stateful/stateful-provider-adapter.js +287 -0
  167. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  168. package/dist/stateful/stateful-provider-observability.js +161 -0
  169. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  170. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  171. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  172. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  173. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  174. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  175. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  176. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  177. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  178. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  179. package/dist/stateful-signing.d.ts +18 -0
  180. package/dist/stateful-signing.js +27 -0
  181. package/dist/stealth/profiles.js +16 -7
  182. package/dist/stream-evidence.d.ts +74 -0
  183. package/dist/stream-evidence.js +785 -0
  184. package/dist/stream.js +7 -1
  185. package/dist/testing/index.d.ts +2 -1
  186. package/dist/testing/index.js +2 -1
  187. package/dist/testing/run.d.ts +32 -2
  188. package/dist/testing/run.js +489 -21
  189. package/dist/trace-sanitization.d.ts +5 -0
  190. package/dist/trace-sanitization.js +45 -0
  191. package/dist/types.d.ts +545 -23
  192. package/dist/types.js +1 -0
  193. package/package.json +44 -5
  194. package/src/auth-turn/index.ts +1 -1
  195. package/src/auth.ts +118 -0
  196. package/src/ceremonies/index.ts +189 -46
  197. package/src/cli/commands.ts +10 -0
  198. package/src/cli/create.ts +42 -35
  199. package/src/cli/prompt-assets.ts +865 -0
  200. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  201. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  202. package/src/cli/templates/provider/README.md.tpl +4 -4
  203. package/src/cli/templates/provider/index.ts.tpl +6 -3
  204. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  205. package/src/config/loader.ts +665 -163
  206. package/src/contract-serialization.ts +5 -7
  207. package/src/contract-types.ts +1 -0
  208. package/src/contract.ts +2 -0
  209. package/src/declaration-validation.ts +266 -0
  210. package/src/define.ts +970 -87
  211. package/src/error-resolution.ts +127 -0
  212. package/src/errors.ts +52 -0
  213. package/src/fixture-sanitization.ts +248 -0
  214. package/src/health-scenario.ts +875 -0
  215. package/src/index.ts +204 -8
  216. package/src/lint.ts +408 -4
  217. package/src/native-address.ts +340 -0
  218. package/src/native-egress-policy.ts +358 -0
  219. package/src/observability.ts +51 -1
  220. package/src/provider.ts +133 -0
  221. package/src/runtime/auth-flow.ts +12 -0
  222. package/src/runtime/browser.ts +661 -63
  223. package/src/runtime/cache.ts +189 -14
  224. package/src/runtime/choice-wordlist.ts +145 -0
  225. package/src/runtime/choice.ts +631 -120
  226. package/src/runtime/executor.ts +40 -7
  227. package/src/runtime/http.ts +641 -61
  228. package/src/runtime/instrumentation.ts +520 -15
  229. package/src/runtime/native-network-errors.ts +99 -0
  230. package/src/runtime/native-network.ts +1605 -0
  231. package/src/runtime/ocr.ts +523 -0
  232. package/src/runtime/proxy-errors.ts +12 -4
  233. package/src/runtime/proxy-nodemaven.ts +221 -0
  234. package/src/runtime/proxy-telemetry.ts +244 -75
  235. package/src/runtime/redirects.ts +66 -0
  236. package/src/runtime/redis.ts +7 -2
  237. package/src/runtime/request-options.ts +680 -1
  238. package/src/runtime/resolver-config.ts +6 -0
  239. package/src/runtime/resolver-public.ts +20 -0
  240. package/src/runtime/resolver-shared.ts +17 -0
  241. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  242. package/src/runtime/resolver-vendors/browser.ts +533 -0
  243. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  244. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  245. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  246. package/src/runtime/resolver-vendors/types.ts +212 -0
  247. package/src/runtime/resolver.ts +1103 -0
  248. package/src/runtime/secrets.ts +64 -0
  249. package/src/runtime/state.ts +394 -77
  250. package/src/runtime/stealth-cookies.ts +132 -0
  251. package/src/runtime/stealth.ts +675 -289
  252. package/src/runtime/stt.ts +1 -19
  253. package/src/runtime/timeout.ts +18 -0
  254. package/src/runtime/trace-config.ts +77 -0
  255. package/src/serve.ts +6 -1
  256. package/src/server/index.ts +37 -2
  257. package/src/server/self-test-input-tokens.ts +29 -14
  258. package/src/server/self-test.ts +1025 -175
  259. package/src/server/serve-implementation.ts +3250 -0
  260. package/src/server/serve.ts +1 -1626
  261. package/src/server/trace-output.ts +32 -0
  262. package/src/server/types.ts +13 -1
  263. package/src/stateful/README.md +146 -0
  264. package/src/stateful/errors.ts +35 -0
  265. package/src/stateful/http-provider-event-emitter.ts +314 -0
  266. package/src/stateful/http-session-owner-registry.ts +306 -0
  267. package/src/stateful/index.ts +18 -0
  268. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  269. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  270. package/src/stateful/provider-event-pipeline.ts +61 -0
  271. package/src/stateful/provider-events.ts +462 -0
  272. package/src/stateful/session-key.ts +111 -0
  273. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  274. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  275. package/src/stateful/stateful-provider-adapter.ts +562 -0
  276. package/src/stateful/stateful-provider-observability.ts +261 -0
  277. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  278. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  279. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  280. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  281. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  282. package/src/stateful-signing.ts +46 -0
  283. package/src/stealth/profiles.ts +17 -7
  284. package/src/stream-evidence.ts +988 -0
  285. package/src/stream.ts +8 -1
  286. package/src/testing/index.ts +10 -1
  287. package/src/testing/run.ts +658 -15
  288. package/src/trace-sanitization.ts +63 -0
  289. package/src/types.ts +660 -38
  290. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  291. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  292. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  293. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  294. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  295. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  296. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  297. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  298. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  299. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  300. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  301. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  302. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  303. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,20 +1,38 @@
1
1
  import { createHash } from "node:crypto";
2
- import { Impit } from "impit";
3
- import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader.js";
2
+ import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, policyResolvesRegistryVendorChain, resolvePolicyProxyPoolSpan, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, vendorFromResolvedSource, } from "../config/loader.js";
4
3
  import { SDKError, TransportError } from "../errors.js";
5
4
  import { getStealthProfile } from "../stealth/profiles.js";
5
+ import { StealthCookieJar } from "./stealth-cookies.js";
6
6
  import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
7
7
  import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
8
- import { appendQueryParams } from "./request-options.js";
9
- const DEFAULT_PROFILE = "chrome-146";
8
+ import { evaluateRedirectHop, isRedirectStatus, nextRedirectMethod, resolveRedirectUrl, } from "./redirects.js";
9
+ import { isSensitiveKey, normalizeSensitiveParams, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, serializeRequestUrl, } from "./request-options.js";
10
+ export const DEFAULT_PROFILE = "chrome-146";
10
11
  const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
11
- const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
12
12
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
13
13
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
14
14
  const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
15
15
  const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
16
16
  const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
17
17
  const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
18
+ const MAX_STEALTH_REDIRECT_HOPS = 10;
19
+ const REDIRECT_BODY_HEADERS = new Set([
20
+ "content-encoding",
21
+ "content-language",
22
+ "content-location",
23
+ "content-type",
24
+ ]);
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null;
27
+ }
28
+ function sensitiveQueryParamNames(url) {
29
+ const queryStart = url.indexOf("?");
30
+ if (queryStart === -1)
31
+ return [];
32
+ const fragmentStart = url.indexOf("#", queryStart);
33
+ const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
34
+ return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
35
+ }
18
36
  const REMOVED_CHROME_PROFILE_NAMES = new Set([
19
37
  "chrome-120",
20
38
  "chrome-124",
@@ -28,133 +46,93 @@ const REMOVED_CHROME_PROFILE_NAMES = new Set([
28
46
  "chrome-130-psk",
29
47
  "edge-131",
30
48
  ]);
31
- const CHROME_IMPIT_BY_MAJOR = {
32
- 100: "chrome100",
33
- 101: "chrome101",
34
- 104: "chrome104",
35
- 107: "chrome107",
36
- 110: "chrome110",
37
- 116: "chrome116",
38
- 124: "chrome124",
39
- 125: "chrome125",
40
- 131: "chrome131",
41
- 136: "chrome136",
42
- 142: "chrome142",
43
- };
44
- const FIREFOX_IMPIT_BY_MAJOR = {
45
- 128: "firefox128",
46
- 133: "firefox133",
47
- 135: "firefox135",
48
- 144: "firefox144",
49
- };
50
- function isRecord(value) {
51
- return typeof value === "object" && value !== null;
52
- }
53
- class CookieJarImpl {
54
- cookies;
55
- constructor(cookieStrings) {
56
- this.cookies = {};
57
- this.setFromCookieStrings(cookieStrings);
58
- }
59
- setFromCookieStrings(cookieStrings) {
60
- for (const cookieString of cookieStrings) {
61
- const [nameValue] = cookieString.split(";");
62
- if (!nameValue) {
63
- continue;
64
- }
65
- const separatorIndex = nameValue.indexOf("=");
66
- if (separatorIndex === -1) {
67
- continue;
68
- }
69
- const name = nameValue.slice(0, separatorIndex).trim();
70
- const value = nameValue.slice(separatorIndex + 1).trim();
71
- if (name)
72
- this.cookies[name] = value;
73
- }
74
- }
75
- get(name) {
76
- return this.cookies[name];
77
- }
78
- getAll() {
79
- return { ...this.cookies };
80
- }
81
- has(name) {
82
- return Object.hasOwn(this.cookies, name);
83
- }
84
- toString() {
85
- return Object.entries(this.cookies)
86
- .map(([name, value]) => `${name}=${value}`)
87
- .join("; ");
88
- }
89
- toHeader() {
90
- return this.toString();
49
+ let wreqModulePromise;
50
+ function getWreqModule() {
51
+ if (!wreqModulePromise) {
52
+ wreqModulePromise = import("wreq-js").catch((error) => {
53
+ throw new SDKError(`Stealth transport is unavailable on ${process.platform}-${process.arch}: the wreq-js native binary could not be loaded.`, {
54
+ code: "stealth_transport_unavailable",
55
+ cause: error instanceof Error ? error : undefined,
56
+ });
57
+ });
91
58
  }
92
- snapshot() {
93
- return this.getAll();
59
+ return wreqModulePromise;
60
+ }
61
+ function parseProfileIdentifier(identifier) {
62
+ const match = /^(safari_ios|safari_ipad|firefox_android|firefox_private|chrome|edge|firefox|opera|safari|okhttp)_(\d+(?:[._]\d+)*)$/.exec(identifier.toLowerCase());
63
+ if (!match?.[1] || !match[2])
64
+ return null;
65
+ return {
66
+ family: match[1],
67
+ version: match[2].split(/[._]/).map(Number),
68
+ };
69
+ }
70
+ function compareVersionDistance(target, left, right) {
71
+ const width = Math.max(target.length, left.length, right.length);
72
+ for (let index = 0; index < width; index += 1) {
73
+ const targetPart = target[index] ?? 0;
74
+ const leftDistance = Math.abs((left[index] ?? 0) - targetPart);
75
+ const rightDistance = Math.abs((right[index] ?? 0) - targetPart);
76
+ if (leftDistance !== rightDistance)
77
+ return leftDistance - rightDistance;
94
78
  }
95
- restore(cookies) {
96
- this.clear();
97
- for (const [name, value] of Object.entries(cookies)) {
98
- if (name)
99
- this.cookies[name] = value;
79
+ return 0;
80
+ }
81
+ function closestWreqProfile(identifier, wreqProfiles) {
82
+ const requested = parseProfileIdentifier(identifier);
83
+ if (!requested)
84
+ return undefined;
85
+ let closest;
86
+ for (const candidateName of wreqProfiles) {
87
+ const candidate = parseProfileIdentifier(candidateName);
88
+ if (!candidate || candidate.family !== requested.family)
89
+ continue;
90
+ if (!closest ||
91
+ compareVersionDistance(requested.version, candidate.version, closest.version) < 0) {
92
+ closest = { name: candidateName, version: candidate.version };
100
93
  }
101
94
  }
102
- clear() {
103
- for (const name of Object.keys(this.cookies)) {
104
- delete this.cookies[name];
105
- }
95
+ return closest?.name;
96
+ }
97
+ function resolveDefaultWreqProfileMapping() {
98
+ let profile;
99
+ try {
100
+ profile = getStealthProfile(DEFAULT_PROFILE);
106
101
  }
107
- find(predicate) {
108
- for (const [name, value] of Object.entries(this.cookies)) {
109
- const cookie = `${name}=${value}`;
110
- if (predicate(cookie)) {
111
- return cookie;
112
- }
113
- }
114
- return undefined;
102
+ catch (error) {
103
+ throw new SDKError(`Default stealth profile "${DEFAULT_PROFILE}" cannot be mapped to a wreq-js browser profile.`, { cause: error instanceof Error ? error : undefined });
115
104
  }
116
- }
117
- function closestImpitBrowser(major, candidates) {
118
- let closestMajor;
119
- let closestBrowser;
120
- for (const [candidateMajorText, browser] of Object.entries(candidates)) {
121
- const candidateMajor = Number(candidateMajorText);
122
- if (closestMajor === undefined ||
123
- Math.abs(candidateMajor - major) < Math.abs(closestMajor - major)) {
124
- closestMajor = candidateMajor;
125
- closestBrowser = browser;
126
- }
105
+ const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
106
+ if (!parseProfileIdentifier(identifier)) {
107
+ throw new SDKError(`Default stealth profile "${DEFAULT_PROFILE}" cannot be mapped to a wreq-js browser profile.`);
127
108
  }
128
- return closestBrowser ?? "chrome142";
109
+ return { identifier, os: profile.platform };
129
110
  }
130
- function resolveImpitBrowser(profileName) {
111
+ const DEFAULT_WREQ_PROFILE_MAPPING = resolveDefaultWreqProfileMapping();
112
+ export function resolveWreqProfile(profileName, wreqProfiles) {
131
113
  if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
132
114
  throw new SDKError(`Unknown stealth profile: ${profileName}`);
133
115
  }
134
- let profile;
116
+ let identifier;
117
+ let os;
135
118
  try {
136
- profile = getStealthProfile(profileName);
119
+ const profile = getStealthProfile(profileName);
120
+ identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
121
+ os = profile.platform;
137
122
  }
138
123
  catch {
139
124
  // Preserve the previous ctx.stealth.fetch() compatibility behavior: unknown
140
125
  // profile strings still run with the transport default instead of failing
141
126
  // before the request starts. Removed built-in profile aliases above remain
142
127
  // explicit errors so callers do not accidentally pin retired fingerprints.
143
- return "chrome142";
128
+ identifier = DEFAULT_WREQ_PROFILE_MAPPING.identifier;
129
+ os = DEFAULT_WREQ_PROFILE_MAPPING.os;
144
130
  }
145
- const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
146
- const chromeMatch = /^(?:chrome|edge)_(\d+)/.exec(identifier);
147
- if (chromeMatch?.[1]) {
148
- return closestImpitBrowser(Number(chromeMatch[1]), CHROME_IMPIT_BY_MAJOR);
131
+ const browser = closestWreqProfile(identifier, wreqProfiles);
132
+ if (!browser) {
133
+ throw new SDKError(`Stealth profile "${profileName}" cannot be mapped to a wreq-js browser profile.`);
149
134
  }
150
- const firefoxMatch = /^firefox_(\d+)/.exec(identifier);
151
- if (firefoxMatch?.[1]) {
152
- return closestImpitBrowser(Number(firefoxMatch[1]), FIREFOX_IMPIT_BY_MAJOR);
153
- }
154
- if (identifier.startsWith("safari_")) {
155
- throw new SDKError(`Stealth profile "${profileName}" uses a Safari stealth fingerprint, but TypeScript ctx.stealth uses impit which currently supports Chrome, Firefox, and OkHttp profiles only. Use a Chrome/Firefox stealth profile for ctx.stealth or ctx.browser for Safari-specific behavior.`);
156
- }
157
- throw new SDKError(`Stealth profile "${profileName}" cannot be mapped to an impit browser profile.`);
135
+ return { browser, os };
158
136
  }
159
137
  function resolveUrl(baseUrl, url) {
160
138
  return new URL(url, baseUrl).toString();
@@ -174,18 +152,6 @@ function normalizeHeaders(headers) {
174
152
  function hasOwn(object, key) {
175
153
  return Object.hasOwn(object, key);
176
154
  }
177
- function toImpitCookieJar(cookieJar) {
178
- return {
179
- setCookie(cookie, _url, cb) {
180
- cookieJar.setFromCookieStrings([cookie]);
181
- if (typeof cb === "function")
182
- cb();
183
- },
184
- getCookieString(_url) {
185
- return cookieJar.toString();
186
- },
187
- };
188
- }
189
155
  function assertNoUnsupportedFingerprintOverrides(options) {
190
156
  if (!isRecord(options))
191
157
  return;
@@ -199,7 +165,7 @@ function assertNoUnsupportedFingerprintOverrides(options) {
199
165
  unsupported.push("stealth.h2");
200
166
  if (unsupported.length === 0)
201
167
  return;
202
- throw new SDKError(`ctx.stealth.fetch uses impit-managed browser fingerprints and no longer accepts low-level stealth overrides: ${unsupported.join(", ")}. Use the profile option instead.`);
168
+ throw new SDKError(`ctx.stealth.fetch uses transport-managed browser fingerprints and no longer accepts low-level stealth overrides: ${unsupported.join(", ")}. Use the profile option instead.`);
203
169
  }
204
170
  function responseHeadersToRecord(headers) {
205
171
  const record = {};
@@ -234,10 +200,12 @@ function splitCombinedSetCookieHeader(headerValue) {
234
200
  cookieStrings.push(finalCookie);
235
201
  return cookieStrings;
236
202
  }
237
- export async function normalizeResponse(response, requestUrl) {
203
+ export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
238
204
  const headers = Object.fromEntries(response.headers.entries());
239
- const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
240
- const bodyBytes = await response.arrayBuffer();
205
+ const cookies = new StealthCookieJar(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
206
+ const bodyBytes = maxBodyBytes === undefined
207
+ ? await response.arrayBuffer()
208
+ : await readResponseBodyWithLimit(response, maxBodyBytes);
241
209
  const body = new TextDecoder().decode(bodyBytes);
242
210
  return {
243
211
  status: response.status,
@@ -263,6 +231,63 @@ export async function normalizeResponse(response, requestUrl) {
263
231
  },
264
232
  };
265
233
  }
234
+ function responseTooLargeError(maxBodyBytes, observedBytes) {
235
+ return new TransportError(`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`, {
236
+ code: "response_too_large",
237
+ category: "upstream_http",
238
+ retryable: false,
239
+ status: 0,
240
+ });
241
+ }
242
+ function declaredContentLength(headers) {
243
+ const contentLength = headers.get("content-length")?.trim();
244
+ if (!contentLength || !/^\d+$/.test(contentLength))
245
+ return undefined;
246
+ const parsed = Number(contentLength);
247
+ return Number.isFinite(parsed) ? parsed : undefined;
248
+ }
249
+ async function readResponseBodyWithLimit(response, maxBodyBytes) {
250
+ const contentLength = declaredContentLength(response.headers);
251
+ if (contentLength !== undefined && contentLength > maxBodyBytes) {
252
+ await response.body?.cancel().catch(() => undefined);
253
+ throw responseTooLargeError(maxBodyBytes, contentLength);
254
+ }
255
+ if (!response.body) {
256
+ throw new TransportError("Response body stream is unavailable", {
257
+ code: "transport_stream_unavailable",
258
+ category: "upstream_http",
259
+ status: 0,
260
+ });
261
+ }
262
+ const reader = response.body.getReader();
263
+ const chunks = [];
264
+ let receivedBytes = 0;
265
+ try {
266
+ while (true) {
267
+ const { done, value } = await reader.read();
268
+ if (done)
269
+ break;
270
+ if (!value)
271
+ continue;
272
+ receivedBytes += value.byteLength;
273
+ if (receivedBytes > maxBodyBytes) {
274
+ await reader.cancel().catch(() => undefined);
275
+ throw responseTooLargeError(maxBodyBytes, receivedBytes);
276
+ }
277
+ chunks.push(value);
278
+ }
279
+ }
280
+ finally {
281
+ reader.releaseLock();
282
+ }
283
+ const bodyBytes = new Uint8Array(receivedBytes);
284
+ let offset = 0;
285
+ for (const chunk of chunks) {
286
+ bodyBytes.set(chunk, offset);
287
+ offset += chunk.byteLength;
288
+ }
289
+ return bodyBytes.buffer;
290
+ }
266
291
  function normalizeBody(body) {
267
292
  if (body === undefined) {
268
293
  return "";
@@ -420,16 +445,6 @@ function normalizeMethod(method) {
420
445
  throw new SDKError(`Unsupported stealth method: ${method}`);
421
446
  }
422
447
  }
423
- function isRedirectStatus(status) {
424
- return [301, 302, 303, 307, 308].includes(status);
425
- }
426
- function nextRedirectMethod(status, method) {
427
- if (status === 303 && method !== "HEAD")
428
- return "GET";
429
- if ((status === 301 || status === 302) && method === "POST")
430
- return "GET";
431
- return method;
432
- }
433
448
  function locationHeader(headers) {
434
449
  for (const [name, value] of Object.entries(headers)) {
435
450
  if (name.toLowerCase() === "location")
@@ -437,43 +452,153 @@ function locationHeader(headers) {
437
452
  }
438
453
  return undefined;
439
454
  }
455
+ function withoutRedirectBodyHeaders(headers) {
456
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !REDIRECT_BODY_HEADERS.has(name.toLowerCase())));
457
+ }
458
+ function assertStealthRedirectUrl(url) {
459
+ const protocol = new URL(url).protocol;
460
+ if (protocol !== "http:" && protocol !== "https:") {
461
+ throw new TransportError(`Stealth redirect target scheme "${protocol}" is not allowed`, {
462
+ code: "transport_invalid_url",
463
+ status: 0,
464
+ });
465
+ }
466
+ }
467
+ function discardStealthRedirectBody(response) {
468
+ try {
469
+ const cancellation = response.body?.cancel();
470
+ if (cancellation)
471
+ void cancellation.catch(() => undefined);
472
+ }
473
+ catch {
474
+ // Redirect handling is decided from status and headers. A cancellation
475
+ // failure must not replace or delay that decision.
476
+ }
477
+ }
478
+ async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options) {
479
+ let currentUrl = requestUrl;
480
+ let currentMethod = method;
481
+ let currentBody = options.body === undefined ? undefined : normalizeBody(options.body);
482
+ let currentHeaders = { ...(options.headers ?? {}) };
483
+ let followedHops = 0;
484
+ let response;
485
+ const deadline = options.timeout ? performance.now() + options.timeout : undefined;
486
+ while (true) {
487
+ const headers = { ...currentHeaders };
488
+ if (!hasHeader(headers, "Cookie")) {
489
+ const cookieHeader = cookieJar.toHeader(currentUrl);
490
+ if (cookieHeader)
491
+ headers.Cookie = cookieHeader;
492
+ }
493
+ const requestInit = {
494
+ headers: normalizeHeaders(headers),
495
+ method: currentMethod,
496
+ redirect: "manual",
497
+ };
498
+ if (currentBody !== undefined)
499
+ requestInit.body = currentBody;
500
+ await transport.clearCookies();
501
+ const remainingTimeout = deadline === undefined ? undefined : Math.ceil(deadline - performance.now());
502
+ if (remainingTimeout !== undefined && remainingTimeout <= 0) {
503
+ throw new TransportError("Request timed out", {
504
+ code: "transport_timeout",
505
+ status: 0,
506
+ });
507
+ }
508
+ if (remainingTimeout !== undefined)
509
+ requestInit.timeout = remainingTimeout;
510
+ response = await transport.fetch(currentUrl, requestInit);
511
+ cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? currentUrl);
512
+ if (!isRedirectStatus(response.status) || options.redirect === "manual")
513
+ break;
514
+ if (options.redirect === "error") {
515
+ discardStealthRedirectBody(response);
516
+ throw new TransportError("Stealth request encountered a redirect", {
517
+ code: "transport_network_error",
518
+ status: 0,
519
+ });
520
+ }
521
+ const nextUrl = resolveRedirectUrl(response.headers.get("location") ?? undefined, response.url ?? currentUrl);
522
+ if (!nextUrl)
523
+ break;
524
+ if (followedHops >= MAX_STEALTH_REDIRECT_HOPS) {
525
+ discardStealthRedirectBody(response);
526
+ throw new TransportError(`Stealth request exceeded the ${MAX_STEALTH_REDIRECT_HOPS}-redirect limit`, { code: "transport_network_error", status: 0 });
527
+ }
528
+ assertStealthRedirectUrl(nextUrl);
529
+ discardStealthRedirectBody(response);
530
+ const nextMethod = nextRedirectMethod(response.status, currentMethod);
531
+ if (nextMethod !== currentMethod) {
532
+ currentBody = undefined;
533
+ currentHeaders = withoutRedirectBodyHeaders(currentHeaders);
534
+ }
535
+ currentMethod = nextMethod;
536
+ currentUrl = nextUrl;
537
+ followedHops += 1;
538
+ }
539
+ const normalized = await normalizeResponse(response, currentUrl, options.maxBodyBytes);
540
+ if (followedHops > 0)
541
+ normalized.redirected = true;
542
+ return { normalized, response };
543
+ }
440
544
  function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
441
545
  const clients = new Map();
442
546
  let closed = false;
443
547
  let hasWarnedMissingProxy = false;
444
548
  const warn = clientOptions.warn ?? console.warn;
445
- const cookieJar = new CookieJarImpl([]);
446
- const impitCookieJar = toImpitCookieJar(cookieJar);
447
- function getClient(profileName, proxyUrl, ignoreTlsErrors) {
549
+ const cookieJar = new StealthCookieJar([], baseUrl);
550
+ async function getClientEntry(profileName, proxyUrl, ignoreTlsErrors) {
448
551
  if (closed) {
449
552
  throw new TransportError("Stealth session is closed", { status: 0 });
450
553
  }
451
- const browser = resolveImpitBrowser(profileName);
554
+ const wreq = await getWreqModule();
555
+ const { browser, os } = resolveWreqProfile(profileName, wreq.getProfiles());
452
556
  const cacheKey = JSON.stringify({ browser, proxyUrl, ignoreTlsErrors });
453
- let client = clients.get(cacheKey);
454
- if (!client) {
455
- client = new Impit({
456
- browser,
457
- cookieJar: impitCookieJar,
458
- ...(proxyUrl ? { proxyUrl } : {}),
459
- ...(ignoreTlsErrors ? { ignoreTlsErrors: true } : {}),
460
- timeout: 30_000,
461
- });
462
- clients.set(cacheKey, client);
557
+ let entry = clients.get(cacheKey);
558
+ if (!entry) {
559
+ entry = {
560
+ session: wreq.createSession({
561
+ browser,
562
+ os,
563
+ ...(proxyUrl ? { proxy: proxyUrl } : {}),
564
+ ...(ignoreTlsErrors ? { insecure: true } : {}),
565
+ timeout: 30_000,
566
+ }),
567
+ tail: Promise.resolve(),
568
+ };
569
+ clients.set(cacheKey, entry);
570
+ }
571
+ return entry;
572
+ }
573
+ async function withClient(profileName, proxyUrl, ignoreTlsErrors, operation) {
574
+ const entry = await getClientEntry(profileName, proxyUrl, ignoreTlsErrors);
575
+ const previous = entry.tail;
576
+ let release;
577
+ entry.tail = new Promise((resolve) => {
578
+ release = resolve;
579
+ });
580
+ await previous;
581
+ try {
582
+ return await operation(await entry.session);
583
+ }
584
+ finally {
585
+ release();
463
586
  }
464
- return client;
465
587
  }
466
- async function resolveRequestProxy(options, proxyAttempt) {
588
+ async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
467
589
  const resolvedProxy = await resolveProxyConfigAsync({
468
590
  proxy: options?.proxy ?? clientOptions.proxy,
469
591
  upstream: clientOptions.upstream,
470
- apifuseConfig: clientOptions.apifuseConfig,
471
592
  affinityKey: clientOptions.affinityKey,
472
593
  proxyAttempt: computeProxyAttemptIndex({
473
594
  baseProxyAttempt: clientOptions.proxyAttempt,
474
595
  proxyAttemptOffset: options?.proxyAttemptOffset,
475
596
  retryAttemptOffset: proxyAttempt,
476
597
  }),
598
+ // The stealth transport tunnels both HTTP CONNECT and SOCKS5,
599
+ // preserving the client TLS fingerprint end-to-end.
600
+ transportProtocols: ["http", "socks5"],
601
+ ...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
477
602
  telemetry: clientOptions.telemetry,
478
603
  });
479
604
  if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
@@ -484,34 +609,57 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
484
609
  url: resolvedProxy.url,
485
610
  poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
486
611
  proxyHash: proxyEndpointHash(resolvedProxy.url),
612
+ vendor: vendorFromResolvedSource(resolvedProxy.source),
487
613
  };
488
614
  }
489
615
  const session = {
490
616
  async fetch(url, options = {}) {
491
- const method = normalizeMethod(options.method ?? "GET");
492
- const hasExplicitRetryPolicy = options.retry !== undefined;
493
- const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
494
- extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
495
- label: "Stealth",
496
- }) ??
497
- (hasExplicitRetryPolicy
498
- ? undefined
499
- : createDefaultProxyTransportRetryOptions({
617
+ const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
618
+ try {
619
+ const method = normalizeMethod(options.method ?? "GET");
620
+ const hasExplicitRetryPolicy = options.retry !== undefined;
621
+ const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
500
622
  extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
501
623
  label: "Stealth",
502
- }));
503
- if (stealthRetryOptions) {
504
- validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
505
- }
624
+ }) ??
625
+ (hasExplicitRetryPolicy
626
+ ? undefined
627
+ : createDefaultProxyTransportRetryOptions({
628
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
629
+ label: "Stealth",
630
+ }));
631
+ if (stealthRetryOptions) {
632
+ validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
633
+ }
634
+ return { hasExplicitRetryPolicy, method, stealthRetryOptions };
635
+ }
636
+ catch (error) {
637
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
638
+ }
639
+ })();
506
640
  const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
507
641
  const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
508
642
  const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
509
- const policyProxyAttemptCap = Math.max(1, Math.min(MAX_POLICY_PROXY_RETRY_ATTEMPTS, clientOptions.proxyPolicy?.session?.poolSize ??
643
+ // Span the whole vendor chain: successive attempts rotate one vendor's
644
+ // pool, then fail over to the next vendor via the flat attempt index.
645
+ const policyProxy = clientOptions.proxyPolicy ??
510
646
  (typeof clientOptions.upstream?.proxy === "object"
511
- ? clientOptions.upstream.proxy.session?.poolSize
512
- : undefined) ??
513
- DEFAULT_SMARTPROXY_POOL_SIZE));
514
- const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
647
+ ? clientOptions.upstream.proxy
648
+ : undefined);
649
+ // The pool span is already bounded by each vendor's max pool size
650
+ // (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
651
+ // the chain's true maximum — a large NodeMaven pool stays fully
652
+ // reachable rather than being truncated at an arbitrary ceiling.
653
+ const policyProxyAttemptCap = Math.max(1, policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE);
654
+ // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
655
+ // successive attempts resolve a *different* endpoint, so it is the only one
656
+ // that may widen the attempt cap to the pool span, de-duplicate endpoints,
657
+ // and drive allocator stale-pool refresh. Deprecated custom/decodo policies
658
+ // have no managed endpoint to rotate or refresh, so they follow the ordinary
659
+ // transport-retry budget instead.
660
+ const rotatesRegistryChain = usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
661
+ const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
662
+ const dedupeAllocatorEndpoints = rotatesRegistryChain;
515
663
  let lastError;
516
664
  for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
517
665
  let stalePoolError;
@@ -520,6 +668,11 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
520
668
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
521
669
  let proxy;
522
670
  let attemptProxy;
671
+ // Reuse the exact serialization used by this outbound attempt in its catch path.
672
+ let serializedUrl;
673
+ let fallbackSensitiveValues = [];
674
+ let fallbackRequestUrl;
675
+ let fallbackRedactedUrl;
523
676
  const attemptStartedAt = Date.now();
524
677
  let attemptRecorded = false;
525
678
  const recordProxyAttempt = (outcome, errorCode, status) => {
@@ -527,7 +680,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
527
680
  return;
528
681
  attemptRecorded = true;
529
682
  clientOptions.telemetry?.recordProxyAttempt?.({
530
- provider: "smartproxy",
683
+ provider: attemptProxy?.vendor ?? "smartproxy",
531
684
  attempt: attempt + 1,
532
685
  ...(attemptProxy?.poolIndex === undefined
533
686
  ? {}
@@ -540,37 +693,36 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
540
693
  });
541
694
  };
542
695
  try {
696
+ const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
697
+ const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
698
+ fallbackSensitiveValues = [
699
+ ...new Set([
700
+ ...Object.values(sensitiveParams ?? {}).map(String),
701
+ ...structural.sensitiveValues,
702
+ ]),
703
+ ].filter((value) => value !== "");
704
+ fallbackRequestUrl = url;
705
+ fallbackRedactedUrl = structural.redactedUrl;
543
706
  assertNoUnsupportedFingerprintOverrides(options);
544
- attemptProxy = await resolveRequestProxy(options, attempt);
707
+ attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
545
708
  proxy = attemptProxy.url;
546
- if (proxy && usesPolicyAllocator) {
709
+ if (proxy && dedupeAllocatorEndpoints) {
710
+ // An under-filled allocation repeats endpoints (via the modulo
711
+ // pool mapping) before the flat offset crosses into the next
712
+ // vendor. Skip an already-tried endpoint and advance the offset
713
+ // rather than breaking — breaking here would strand the request on
714
+ // the primary vendor and never reach the fallback leg.
547
715
  if (attemptedProxies.has(proxy)) {
548
- break;
716
+ continue;
549
717
  }
550
718
  attemptedProxies.add(proxy);
551
719
  }
552
720
  const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
553
721
  (!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
554
722
  const profileName = options.profile ?? defaultProfile;
555
- const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
556
- const headers = { ...(options.headers ?? {}) };
557
- if (!hasHeader(headers, "Cookie")) {
558
- const cookieHeader = cookieJar.toString();
559
- if (cookieHeader)
560
- headers.Cookie = cookieHeader;
561
- }
562
- const requestInit = {
563
- headers: normalizeHeaders(headers),
564
- method,
565
- ...(options.redirect ? { redirect: options.redirect } : {}),
566
- ...(options.timeout ? { timeout: options.timeout } : {}),
567
- };
568
- if (options.body !== undefined) {
569
- requestInit.body = normalizeBody(options.body);
570
- }
571
- const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
572
- const normalized = await normalizeResponse(response, requestUrl);
573
- cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
723
+ serializedUrl = serializeRequestUrl(resolveUrl(baseUrl, url), options.params, sensitiveParams);
724
+ const { requestUrl } = serializedUrl;
725
+ const { normalized, response } = await withClient(profileName, proxy, ignoreTlsErrors, (transport) => fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options));
574
726
  if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
575
727
  throw createProxyConnectFailureError(normalized.body);
576
728
  }
@@ -602,12 +754,23 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
602
754
  return normalized;
603
755
  }
604
756
  catch (error) {
605
- const normalizedError = normalizeStealthTransportError(error);
757
+ const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
758
+ let normalizedError;
759
+ try {
760
+ normalizedError = normalizeStealthTransportError(error);
761
+ }
762
+ catch (normalizationError) {
763
+ throw redactSensitiveError(normalizationError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
764
+ }
765
+ const retryErrorCode = proxyAttemptErrorCode(normalizedError);
766
+ const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
767
+ const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
768
+ normalizedError = redactSensitiveError(normalizedError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
606
769
  recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
607
770
  lastError = normalizedError;
608
- if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
771
+ if (proxy && rotatesRegistryChain && refreshableProxyError) {
609
772
  stalePoolError = normalizedError;
610
- if (shouldRunProxyAuthDiagnostic(normalizedError)) {
773
+ if (runProxyAuthDiagnostic) {
611
774
  stalePoolDiagnosticProxy = proxy;
612
775
  }
613
776
  if (attempt + 1 < maxAttempts) {
@@ -615,12 +778,29 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
615
778
  }
616
779
  break;
617
780
  }
618
- if (attempt + 1 <
619
- (stealthRetryOptions
620
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
621
- : maxAttempts) &&
781
+ // Cap the number of transport retries. For a policy-allocator chain,
782
+ // every attempt resolves a *different* endpoint/vendor (poolIndex
783
+ // rotates across the concatenated vendor pool spans), so a transport
784
+ // failure is a signal to advance to the next endpoint — potentially
785
+ // crossing into the fallback vendor — not to retry the same endpoint.
786
+ // Truncating that rotation at the per-endpoint retry budget would
787
+ // strand the request on the primary vendor and never reach the
788
+ // fallback, since the crossover only happens once the flat attempt
789
+ // index exceeds the primary vendor's pool size (~10-20).
790
+ // resolvePolicyTransportAttemptCap widens to the full chain span only
791
+ // for implicit, safe-method allocator requests; explicit retry
792
+ // policies (their documented `attempts` ceiling), unsafe methods, and
793
+ // static/non-registry vendors keep the per-endpoint retry budget.
794
+ const transportRetryCap = resolvePolicyTransportAttemptCap({
795
+ policy: policyProxy,
796
+ usesPolicyAllocator,
797
+ retryAttempts: stealthRetryOptions?.attempts ?? 1,
798
+ explicitRetry: hasExplicitRetryPolicy,
799
+ method,
800
+ });
801
+ if (attempt + 1 < transportRetryCap &&
622
802
  shouldRetryProxyTransportAttempt({
623
- error: normalizedError,
803
+ error: { code: retryErrorCode },
624
804
  explicitRetry: hasExplicitRetryPolicy,
625
805
  method,
626
806
  options: stealthRetryOptions,
@@ -634,7 +814,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
634
814
  throw normalizedError;
635
815
  }
636
816
  }
637
- if (usesPolicyAllocator &&
817
+ if (rotatesRegistryChain &&
638
818
  stalePoolError &&
639
819
  refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES) {
640
820
  await invalidateProxyResolutionCacheAsync({
@@ -670,81 +850,153 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
670
850
  const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
671
851
  ? 10
672
852
  : Math.max(0, Math.floor(options.maxHops));
853
+ const { url: _url, maxHops: _maxHops, stopWhen, params, sensitiveParams, ...fetchOptions } = options;
673
854
  const hops = [];
674
- let currentUrl = resolveUrl(baseUrl, options.url);
675
855
  let method = normalizeMethod(options.method ?? "GET");
676
856
  let body = options.body;
677
857
  let response;
678
858
  const visitedRequests = new Set();
679
- const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
859
+ const initialParams = params
860
+ ? Object.fromEntries(Object.entries(params).map(([key, value]) => [
861
+ key,
862
+ Array.isArray(value) ? [...value] : value,
863
+ ]))
864
+ : undefined;
865
+ const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
866
+ const initialSensitiveParams = normalizedSensitiveParams
867
+ ? { ...normalizedSensitiveParams }
868
+ : undefined;
869
+ const sensitiveParamNames = initialSensitiveParams
870
+ ? Object.keys(initialSensitiveParams)
871
+ : [];
872
+ const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
873
+ const sensitiveValues = new Set([
874
+ ...Object.values(initialSensitiveParams ?? {}),
875
+ ...callerStructural.sensitiveValues,
876
+ ].filter((value) => value !== ""));
877
+ const redactRedirectUrl = (value) => {
878
+ const structural = redactUrlQueryParams(value, [
879
+ ...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
880
+ ]);
881
+ for (const sensitiveValue of structural.sensitiveValues) {
882
+ sensitiveValues.add(sensitiveValue);
883
+ }
884
+ return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
885
+ };
886
+ let currentUrl;
887
+ let initialUrl;
888
+ try {
889
+ currentUrl = resolveUrl(baseUrl, options.url);
890
+ redactRedirectUrl(currentUrl);
891
+ initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
892
+ for (const value of initialUrl.sensitiveValues) {
893
+ if (value !== "")
894
+ sensitiveValues.add(value);
895
+ }
896
+ }
897
+ catch (error) {
898
+ throw redactSensitiveError(error, [...sensitiveValues], options.url, redactRedirectUrl(options.url));
899
+ }
680
900
  for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
681
- visitedRequests.add(`${method} ${currentUrl}`);
682
- response = await session.fetch(currentUrl, {
683
- ...fetchOptions,
684
- body,
685
- method,
686
- ...(hopIndex === 0 && params ? { params } : {}),
687
- redirect: "manual",
688
- throwOnHttpError: false,
689
- });
901
+ const outboundUrl = hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
902
+ // Preserve params-only loop bookkeeping from before sensitiveParams:
903
+ // the first visited key is the caller's resolved URL, not its expanded query.
904
+ const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
905
+ visitedRequests.add(`${method} ${visitedUrl}`);
906
+ try {
907
+ response = await session.fetch(currentUrl, {
908
+ ...fetchOptions,
909
+ body,
910
+ method,
911
+ ...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
912
+ ...(hopIndex === 0 && initialSensitiveParams
913
+ ? { sensitiveParams: initialSensitiveParams }
914
+ : {}),
915
+ redirect: "manual",
916
+ throwOnHttpError: false,
917
+ });
918
+ }
919
+ catch (error) {
920
+ throw redactSensitiveError(error, [...sensitiveValues], outboundUrl, redactRedirectUrl(outboundUrl));
921
+ }
922
+ // StealthResponse.url is programmatic metadata and remains raw. Only the
923
+ // redirect hop emitted below is a diagnostic surface.
924
+ const responseUrl = response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
690
925
  if (!isRedirectStatus(response.status)) {
691
926
  return {
692
927
  final: response,
693
928
  hops,
694
929
  reason: "completed",
695
930
  cookies: cookieJar.snapshot(),
931
+ cookieStore: cookieJar.serialize(),
696
932
  };
697
933
  }
698
934
  const location = locationHeader(response.headers);
699
- const nextUrl = location
700
- ? new URL(location, response.url ?? currentUrl).toString()
701
- : undefined;
702
- const hop = {
703
- url: response.url ?? currentUrl,
935
+ const redactedResponseUrl = redactRedirectUrl(responseUrl);
936
+ const redactedLocation = location ? redactRedirectUrl(location) : undefined;
937
+ let nextUrl;
938
+ try {
939
+ nextUrl = resolveRedirectUrl(location, responseUrl);
940
+ }
941
+ catch (error) {
942
+ throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
943
+ }
944
+ const realHop = {
945
+ url: responseUrl,
704
946
  status: response.status,
705
947
  method,
706
948
  ...(location ? { location } : {}),
707
949
  ...(nextUrl ? { nextUrl } : {}),
708
950
  };
951
+ const hop = {
952
+ ...realHop,
953
+ url: redactedResponseUrl,
954
+ ...(redactedLocation ? { location: redactedLocation } : {}),
955
+ ...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
956
+ };
709
957
  hops.push(hop);
710
- if (stopWhen && (await stopWhen(hop))) {
711
- return {
712
- final: response,
713
- hops,
714
- reason: "stopped",
715
- cookies: cookieJar.snapshot(),
716
- };
717
- }
718
- if (!nextUrl) {
719
- return {
720
- final: response,
721
- hops,
722
- reason: "missing_location",
723
- cookies: cookieJar.snapshot(),
724
- };
958
+ let shouldStop = false;
959
+ if (stopWhen) {
960
+ try {
961
+ shouldStop = await stopWhen(realHop);
962
+ }
963
+ catch (error) {
964
+ let sanitizedError = error;
965
+ for (const [rawUrl, safeUrl] of [
966
+ [responseUrl, redactedResponseUrl],
967
+ [location, redactedLocation],
968
+ [nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
969
+ ]) {
970
+ if (!rawUrl || !safeUrl)
971
+ continue;
972
+ sanitizedError = redactSensitiveError(sanitizedError, [...sensitiveValues], rawUrl, safeUrl);
973
+ }
974
+ throw sanitizedError;
975
+ }
725
976
  }
726
- if (hops.length > maxHops) {
977
+ const decision = evaluateRedirectHop({
978
+ status: response.status,
979
+ method,
980
+ nextUrl,
981
+ shouldStop,
982
+ redirectCount: hops.length,
983
+ maxHops,
984
+ visitedRequests,
985
+ });
986
+ if (decision.kind === "stop") {
727
987
  return {
728
988
  final: response,
729
989
  hops,
730
- reason: "max_hops",
990
+ reason: decision.reason,
731
991
  cookies: cookieJar.snapshot(),
992
+ cookieStore: cookieJar.serialize(),
732
993
  };
733
994
  }
734
- const nextMethod = nextRedirectMethod(response.status, method);
735
- if (nextMethod !== method) {
995
+ if (decision.nextMethod !== method) {
736
996
  body = undefined;
737
997
  }
738
- if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
739
- return {
740
- final: response,
741
- hops,
742
- reason: "loop",
743
- cookies: cookieJar.snapshot(),
744
- };
745
- }
746
- method = nextMethod;
747
- currentUrl = nextUrl;
998
+ method = decision.nextMethod;
999
+ currentUrl = decision.nextUrl;
748
1000
  }
749
1001
  if (!response) {
750
1002
  response = await session.fetch(currentUrl, {
@@ -761,23 +1013,35 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
761
1013
  hops,
762
1014
  reason: "max_hops",
763
1015
  cookies: cookieJar.snapshot(),
1016
+ cookieStore: cookieJar.serialize(),
764
1017
  };
765
1018
  },
766
1019
  },
767
1020
  close() {
768
1021
  closed = true;
1022
+ for (const client of clients.values()) {
1023
+ void client.session
1024
+ .then((session) => session.close())
1025
+ .catch((error) => {
1026
+ const message = error instanceof Error ? error.message : String(error);
1027
+ warn(`[provider-sdk] Failed to close stealth transport session: ${message}`);
1028
+ });
1029
+ }
769
1030
  clients.clear();
770
1031
  },
771
1032
  };
772
1033
  return session;
773
1034
  async function classifyProxyAuthDiagnostic(profileName, proxy) {
774
1035
  try {
775
- const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
776
- method: "GET",
777
- timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
1036
+ return await withClient(profileName, proxy, false, async (client) => {
1037
+ await client.clearCookies();
1038
+ const response = await client.fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
1039
+ method: "GET",
1040
+ timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
1041
+ });
1042
+ const normalized = await normalizeResponse(response);
1043
+ return classifyProxyAuthDiagnosticMessage(normalized.body);
778
1044
  });
779
- const normalized = await normalizeResponse(response);
780
- return classifyProxyAuthDiagnosticMessage(normalized.body);
781
1045
  }
782
1046
  catch (error) {
783
1047
  const message = error instanceof Error