@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +90 -0
  3. package/README.md +49 -3
  4. package/SUBMISSION.md +1 -1
  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 +14 -0
  9. package/bin/apifuse-pack-smoke.ts +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  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/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +65 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +176 -8
  32. package/dist/config/loader.js +424 -95
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +9 -1
  39. package/dist/define.js +304 -119
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.d.ts +1 -1
  56. package/dist/lint.js +8 -15
  57. package/dist/native-address.d.ts +43 -0
  58. package/dist/native-address.js +281 -0
  59. package/dist/native-egress-policy.d.ts +31 -0
  60. package/dist/native-egress-policy.js +288 -0
  61. package/dist/observability.d.ts +5 -2
  62. package/dist/observability.js +48 -1
  63. package/dist/provider.d.ts +12 -11
  64. package/dist/provider.js +10 -9
  65. package/dist/public-schema-field-lint.d.ts +1 -1
  66. package/dist/recipes/gov-api.js +1 -1
  67. package/dist/runtime/auth-flow.d.ts +2 -1
  68. package/dist/runtime/auth-flow.js +4 -3
  69. package/dist/runtime/browser.d.ts +1 -1
  70. package/dist/runtime/browser.js +15 -29
  71. package/dist/runtime/cache.d.ts +1 -1
  72. package/dist/runtime/cache.js +4 -8
  73. package/dist/runtime/choice.d.ts +1 -1
  74. package/dist/runtime/choice.js +31 -35
  75. package/dist/runtime/credential.d.ts +1 -1
  76. package/dist/runtime/credential.js +1 -1
  77. package/dist/runtime/env.d.ts +1 -1
  78. package/dist/runtime/executor.d.ts +1 -1
  79. package/dist/runtime/executor.js +31 -4
  80. package/dist/runtime/http.d.ts +2 -2
  81. package/dist/runtime/http.js +387 -47
  82. package/dist/runtime/insights.d.ts +1 -1
  83. package/dist/runtime/insights.js +6 -13
  84. package/dist/runtime/instrumentation.d.ts +2 -2
  85. package/dist/runtime/instrumentation.js +345 -22
  86. package/dist/runtime/keyring.js +1 -1
  87. package/dist/runtime/namespace.js +1 -1
  88. package/dist/runtime/native-network.d.ts +127 -0
  89. package/dist/runtime/native-network.js +1298 -0
  90. package/dist/runtime/otlp.d.ts +1 -1
  91. package/dist/runtime/perf.d.ts +1 -1
  92. package/dist/runtime/provider.d.ts +1 -1
  93. package/dist/runtime/provider.js +1 -2
  94. package/dist/runtime/proxy-errors.d.ts +1 -1
  95. package/dist/runtime/proxy-errors.js +9 -7
  96. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  97. package/dist/runtime/proxy-nodemaven.js +146 -0
  98. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  99. package/dist/runtime/proxy-retry-policy.js +2 -2
  100. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  101. package/dist/runtime/proxy-telemetry.js +55 -52
  102. package/dist/runtime/redirects.d.ts +29 -0
  103. package/dist/runtime/redirects.js +36 -0
  104. package/dist/runtime/redis.d.ts +1 -1
  105. package/dist/runtime/redis.js +2 -4
  106. package/dist/runtime/request-options.d.ts +68 -1
  107. package/dist/runtime/request-options.js +548 -0
  108. package/dist/runtime/secrets.d.ts +27 -0
  109. package/dist/runtime/secrets.js +51 -0
  110. package/dist/runtime/state.d.ts +2 -2
  111. package/dist/runtime/state.js +238 -26
  112. package/dist/runtime/stealth.d.ts +5 -3
  113. package/dist/runtime/stealth.js +423 -143
  114. package/dist/runtime/stt.d.ts +1 -1
  115. package/dist/runtime/stt.js +11 -15
  116. package/dist/runtime/trace.d.ts +2 -2
  117. package/dist/runtime/trace.js +2 -4
  118. package/dist/runtime/waterfall.d.ts +1 -1
  119. package/dist/schema.d.ts +1 -1
  120. package/dist/schema.js +7 -15
  121. package/dist/serve.d.ts +1 -1
  122. package/dist/serve.js +1 -1
  123. package/dist/server/index.d.ts +7 -7
  124. package/dist/server/index.js +6 -6
  125. package/dist/server/self-test-input-tokens.d.ts +2 -1
  126. package/dist/server/self-test-input-tokens.js +18 -14
  127. package/dist/server/self-test-redaction.d.ts +1 -1
  128. package/dist/server/self-test-redaction.js +1 -1
  129. package/dist/server/self-test.d.ts +104 -3
  130. package/dist/server/self-test.js +673 -115
  131. package/dist/server/serve.d.ts +116 -4
  132. package/dist/server/serve.js +799 -128
  133. package/dist/server/types.d.ts +34 -9
  134. package/dist/server/types.js +8 -1
  135. package/dist/stateful/errors.d.ts +14 -0
  136. package/dist/stateful/errors.js +14 -0
  137. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  138. package/dist/stateful/http-provider-event-emitter.js +237 -0
  139. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  140. package/dist/stateful/http-session-owner-registry.js +210 -0
  141. package/dist/stateful/index.d.ts +18 -0
  142. package/dist/stateful/index.js +18 -0
  143. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  144. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  146. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  147. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  148. package/dist/stateful/provider-event-pipeline.js +1 -0
  149. package/dist/stateful/provider-events.d.ts +101 -0
  150. package/dist/stateful/provider-events.js +289 -0
  151. package/dist/stateful/session-key.d.ts +15 -0
  152. package/dist/stateful/session-key.js +86 -0
  153. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  154. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  156. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  157. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  158. package/dist/stateful/stateful-provider-adapter.js +287 -0
  159. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  160. package/dist/stateful/stateful-provider-observability.js +161 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  162. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  163. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  164. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  166. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  167. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  168. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  169. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  170. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  171. package/dist/stateful-signing.d.ts +18 -0
  172. package/dist/stateful-signing.js +27 -0
  173. package/dist/stealth/profiles.d.ts +1 -1
  174. package/dist/stealth/profiles.js +5 -14
  175. package/dist/stream-evidence.d.ts +74 -0
  176. package/dist/stream-evidence.js +785 -0
  177. package/dist/stream.d.ts +1 -1
  178. package/dist/testing/index.d.ts +2 -2
  179. package/dist/testing/index.js +2 -2
  180. package/dist/testing/run.d.ts +32 -2
  181. package/dist/testing/run.js +478 -28
  182. package/dist/types.d.ts +342 -14
  183. package/dist/types.js +1 -0
  184. package/dist/user-input.d.ts +30 -0
  185. package/dist/user-input.js +66 -0
  186. package/package.json +16 -5
  187. package/src/auth-turn/index.ts +1 -1
  188. package/src/auth.ts +28 -86
  189. package/src/ceremonies/index.ts +103 -78
  190. package/src/cli/commands.ts +10 -0
  191. package/src/cli/create.ts +42 -35
  192. package/src/cli/prompt-assets.ts +865 -0
  193. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  194. package/src/config/loader.ts +652 -204
  195. package/src/contract-serialization.ts +5 -11
  196. package/src/contract-types.ts +2 -2
  197. package/src/contract.ts +12 -28
  198. package/src/define.ts +499 -498
  199. package/src/dev.ts +4 -9
  200. package/src/error-resolution.ts +91 -0
  201. package/src/errors.ts +110 -5
  202. package/src/fixture-sanitization.ts +247 -0
  203. package/src/i18n/catalog.ts +10 -32
  204. package/src/i18n/index.ts +2 -2
  205. package/src/i18n/keys.ts +5 -11
  206. package/src/index.ts +112 -42
  207. package/src/lint.ts +88 -152
  208. package/src/native-address.ts +340 -0
  209. package/src/native-egress-policy.ts +358 -0
  210. package/src/observability.ts +51 -1
  211. package/src/provider.ts +66 -11
  212. package/src/public-schema-field-lint.ts +7 -33
  213. package/src/recipes/gov-api.ts +2 -5
  214. package/src/runtime/auth-flow.ts +7 -7
  215. package/src/runtime/browser.ts +64 -187
  216. package/src/runtime/cache.ts +20 -67
  217. package/src/runtime/choice.ts +79 -132
  218. package/src/runtime/credential.ts +2 -2
  219. package/src/runtime/env.ts +1 -1
  220. package/src/runtime/executor.ts +43 -20
  221. package/src/runtime/http.ts +494 -57
  222. package/src/runtime/insights.ts +15 -53
  223. package/src/runtime/instrumentation.ts +495 -66
  224. package/src/runtime/keyring.ts +7 -19
  225. package/src/runtime/namespace.ts +2 -7
  226. package/src/runtime/native-network.ts +1686 -0
  227. package/src/runtime/otlp.ts +12 -23
  228. package/src/runtime/perf.ts +1 -1
  229. package/src/runtime/provider.ts +4 -9
  230. package/src/runtime/proxy-errors.ts +29 -42
  231. package/src/runtime/proxy-nodemaven.ts +221 -0
  232. package/src/runtime/proxy-retry-policy.ts +3 -3
  233. package/src/runtime/proxy-telemetry.ts +79 -77
  234. package/src/runtime/redirects.ts +66 -0
  235. package/src/runtime/redis.ts +4 -12
  236. package/src/runtime/request-options.ts +679 -9
  237. package/src/runtime/secrets.ts +64 -0
  238. package/src/runtime/state.ts +353 -133
  239. package/src/runtime/stealth.ts +505 -154
  240. package/src/runtime/stt.ts +38 -94
  241. package/src/runtime/trace.ts +14 -44
  242. package/src/runtime/waterfall.ts +5 -18
  243. package/src/schema.ts +23 -84
  244. package/src/serve.ts +1 -1
  245. package/src/server/index.ts +29 -7
  246. package/src/server/self-test-input-tokens.ts +29 -14
  247. package/src/server/self-test-redaction.ts +2 -2
  248. package/src/server/self-test.ts +857 -132
  249. package/src/server/serve.ts +1151 -328
  250. package/src/server/types.ts +12 -13
  251. package/src/stateful/README.md +146 -0
  252. package/src/stateful/errors.ts +23 -0
  253. package/src/stateful/http-provider-event-emitter.ts +314 -0
  254. package/src/stateful/http-session-owner-registry.ts +306 -0
  255. package/src/stateful/index.ts +18 -0
  256. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  257. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  258. package/src/stateful/provider-event-pipeline.ts +61 -0
  259. package/src/stateful/provider-events.ts +462 -0
  260. package/src/stateful/session-key.ts +111 -0
  261. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  262. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  263. package/src/stateful/stateful-provider-adapter.ts +562 -0
  264. package/src/stateful/stateful-provider-observability.ts +261 -0
  265. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  266. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  267. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  268. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  269. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  270. package/src/stateful-signing.ts +46 -0
  271. package/src/stealth/profiles.ts +10 -26
  272. package/src/stream-evidence.ts +988 -0
  273. package/src/stream.ts +8 -19
  274. package/src/testing/index.ts +10 -2
  275. package/src/testing/run.ts +653 -74
  276. package/src/types.ts +408 -28
  277. package/src/user-input.ts +118 -0
  278. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  285. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  291. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,26 +1,32 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
3
3
  import { Impit } from "impit";
4
+ import { Cookie, CookieJar as ToughCookieJar } from "tough-cookie";
4
5
 
5
- import type { ProxyResolutionOptions } from "../config/loader";
6
+ import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
6
7
  import {
7
8
  DEFAULT_SMARTPROXY_POOL_SIZE,
8
9
  invalidateProxyResolutionCacheAsync,
9
10
  ProxyResolutionError,
11
+ policyResolvesRegistryVendorChain,
12
+ resolvePolicyProxyPoolSpan,
13
+ resolvePolicyTransportAttemptCap,
10
14
  resolveProxyConfigAsync,
11
- SMARTPROXY_MAX_POOL_SIZE,
12
- } from "../config/loader";
13
- import { SDKError, TransportError } from "../errors";
14
- import { getStealthProfile } from "../stealth/profiles";
15
+ vendorFromResolvedSource,
16
+ } from "../config/loader.js";
17
+ import { SDKError, StealthCookieStoreVersionError, TransportError } from "../errors.js";
18
+ import { getStealthProfile } from "../stealth/profiles.js";
15
19
  import type {
16
20
  CookieJar,
17
21
  HttpMethod,
18
22
  StealthClient,
23
+ StealthCookieStore,
24
+ StealthCookieStoreV1,
19
25
  StealthFetchOptions,
20
26
  StealthRedirectHop,
21
27
  StealthResponse,
22
28
  StealthSession,
23
- } from "../types";
29
+ } from "../types.js";
24
30
  import {
25
31
  createProxyAuthIpDeniedError,
26
32
  createProxyEdgeAuthRejectedError,
@@ -35,7 +41,7 @@ import {
35
41
  isProxyPoolStaleStatus,
36
42
  PROXY_EDGE_AUTH_REJECTED_CODE,
37
43
  PROXY_POOL_STALE_CODE,
38
- } from "./proxy-errors";
44
+ } from "./proxy-errors.js";
39
45
  import {
40
46
  computeProxyAttemptIndex,
41
47
  computeProxyTransportRetryDelayMs,
@@ -43,15 +49,27 @@ import {
43
49
  normalizeProxyTransportRetryOptions,
44
50
  shouldRetryProxyTransportAttempt,
45
51
  validateUnsafeProxyTransportRetryMethods,
46
- } from "./proxy-retry-policy";
47
- import { appendQueryParams } from "./request-options";
52
+ } from "./proxy-retry-policy.js";
53
+ import {
54
+ evaluateRedirectHop,
55
+ isRedirectStatus,
56
+ resolveRedirectUrl,
57
+ } from "./redirects.js";
58
+ import {
59
+ isSensitiveKey,
60
+ redactSensitiveError,
61
+ redactSensitiveRequestError,
62
+ redactSensitiveText,
63
+ redactUrlQueryParams,
64
+ normalizeSensitiveParams,
65
+ serializeRequestUrl,
66
+ } from "./request-options.js";
48
67
 
49
68
  const DEFAULT_PROFILE = "chrome-146";
50
69
 
51
70
  const MISSING_PROXY_WARNING =
52
71
  "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
53
72
 
54
- const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
55
73
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
56
74
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
57
75
  const PROXY_CONNECT_FAILURE_BODY_PATTERN =
@@ -60,6 +78,14 @@ const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
60
78
  const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
61
79
  const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE] as const;
62
80
 
81
+ function sensitiveQueryParamNames(url: string): string[] {
82
+ const queryStart = url.indexOf("?");
83
+ if (queryStart === -1) return [];
84
+ const fragmentStart = url.indexOf("#", queryStart);
85
+ const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
86
+ return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
87
+ }
88
+
63
89
  export type StealthClientOptions = ProxyResolutionOptions & {
64
90
  warn?: (message: string) => void;
65
91
  /**
@@ -112,6 +138,8 @@ type StealthTransportResponse = Pick<
112
138
  ImpitResponse,
113
139
  "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
114
140
  > & {
141
+ body?: ReadableStream<Uint8Array>;
142
+ abort?: () => void;
115
143
  url?: string;
116
144
  redirected?: boolean;
117
145
  };
@@ -125,81 +153,145 @@ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
125
153
  return typeof value === "object" && value !== null;
126
154
  }
127
155
 
156
+ const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
157
+
128
158
  class CookieJarImpl implements CookieJar {
129
- private readonly cookies: Record<string, string>;
159
+ private cookies: ToughCookieJar;
160
+ private readonly defaultUrl: string;
130
161
 
131
- constructor(cookieStrings: string[]) {
132
- this.cookies = {};
162
+ constructor(cookieStrings: readonly string[], defaultUrl = LEGACY_COOKIE_ORIGIN) {
163
+ this.cookies = new ToughCookieJar(undefined, {
164
+ allowSecureOnLocal: false,
165
+ rejectPublicSuffixes: true,
166
+ });
167
+ this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
133
168
  this.setFromCookieStrings(cookieStrings);
134
169
  }
135
170
 
136
- setFromCookieStrings(cookieStrings: readonly string[]): void {
137
- for (const cookieString of cookieStrings) {
138
- const [nameValue] = cookieString.split(";");
139
- if (!nameValue) {
140
- continue;
141
- }
142
-
143
- const separatorIndex = nameValue.indexOf("=");
144
- if (separatorIndex === -1) {
145
- continue;
146
- }
171
+ /**
172
+ * URL-less legacy operations are scoped to this jar's default URL. Session
173
+ * jars use the client's base URL and response jars use the response URL. A
174
+ * flat restore has no attributes to recover, so it creates host-only Path=/
175
+ * cookies for that default URL instead of making them visible to every host.
176
+ */
177
+ setFromCookieStrings(cookieStrings: readonly string[], url = this.defaultUrl): void {
178
+ const cookieUrl = this.normalizeUrl(url);
179
+ if (!cookieUrl) return;
147
180
 
148
- const name = nameValue.slice(0, separatorIndex).trim();
149
- const value = nameValue.slice(separatorIndex + 1).trim();
150
- if (name) this.cookies[name] = value;
181
+ for (const cookieString of cookieStrings) {
182
+ this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
151
183
  }
152
184
  }
153
185
 
154
- get(name: string): string | undefined {
155
- return this.cookies[name];
186
+ get(name: string, url?: string): string | undefined {
187
+ return this.getAll(url)[name];
156
188
  }
157
189
 
158
- getAll(): Record<string, string> {
159
- return { ...this.cookies };
190
+ getAll(url?: string): Record<string, string> {
191
+ return Object.fromEntries(
192
+ this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]),
193
+ );
160
194
  }
161
195
 
162
- has(name: string): boolean {
163
- return Object.hasOwn(this.cookies, name);
196
+ has(name: string, url?: string): boolean {
197
+ return Object.hasOwn(this.getAll(url), name);
164
198
  }
165
199
 
166
- toString(): string {
167
- return Object.entries(this.cookies)
168
- .map(([name, value]) => `${name}=${value}`)
200
+ toString(url?: string): string {
201
+ return this.getUniqueCookies(url ?? this.defaultUrl)
202
+ .map((cookie) => cookie.cookieString())
169
203
  .join("; ");
170
204
  }
171
205
 
172
- toHeader(): string {
173
- return this.toString();
206
+ toHeader(url?: string): string {
207
+ return this.toString(url);
174
208
  }
175
209
 
176
210
  snapshot(): Record<string, string> {
177
- return this.getAll();
211
+ // This compatibility view deliberately enumerates the serialized store,
212
+ // not getAll(defaultUrl): persistence must include sibling hosts and paths.
213
+ // Duplicate names still collapse because a flat map cannot represent them.
214
+ const entries: [string, string][] = [];
215
+ for (const cookie of this.serialize().jar.cookies) {
216
+ if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
217
+ entries.push([cookie.key, cookie.value]);
218
+ }
219
+ }
220
+ return Object.fromEntries(entries);
178
221
  }
179
222
 
180
223
  restore(cookies: Record<string, string>): void {
181
224
  this.clear();
182
225
  for (const [name, value] of Object.entries(cookies)) {
183
- if (name) this.cookies[name] = value;
226
+ if (!name) continue;
227
+ this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
228
+ ignoreError: true,
229
+ });
184
230
  }
185
231
  }
186
232
 
187
- clear(): void {
188
- for (const name of Object.keys(this.cookies)) {
189
- delete this.cookies[name];
233
+ serialize(): StealthCookieStoreV1 {
234
+ const jar = this.cookies.serializeSync();
235
+ if (!jar) {
236
+ throw new SDKError("Stealth cookie store could not be serialized", {
237
+ code: "stealth_cookie_store_serialize_failed",
238
+ });
239
+ }
240
+ return { version: 1, jar };
241
+ }
242
+
243
+ deserialize(state: StealthCookieStore): void {
244
+ const version = isRecord(state) ? state.version : undefined;
245
+ if (version !== 1) {
246
+ throw new StealthCookieStoreVersionError(version);
190
247
  }
248
+
249
+ // Deserialize into a new jar first so invalid state cannot partially clear
250
+ // or replace a live session. tough-cookie restores the cookie attributes and
251
+ // matching semantics represented in its own serialized format.
252
+ const restored = ToughCookieJar.deserializeSync(state.jar);
253
+ // tough-cookie 6 does not include this option in serializeSync(). Preserve
254
+ // the SDK's stricter setting across restoration.
255
+ Reflect.set(restored, "allowSecureOnLocal", false);
256
+ this.cookies = restored;
257
+ }
258
+
259
+ clear(): void {
260
+ this.cookies.removeAllCookiesSync();
191
261
  }
192
262
 
193
- find(predicate: (cookie: string) => boolean): string | undefined {
194
- for (const [name, value] of Object.entries(this.cookies)) {
195
- const cookie = `${name}=${value}`;
196
- if (predicate(cookie)) {
197
- return cookie;
263
+ find(predicate: (cookie: string) => boolean, url?: string): string | undefined {
264
+ for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
265
+ const cookieString = cookie.cookieString();
266
+ if (predicate(cookieString)) {
267
+ return cookieString;
198
268
  }
199
269
  }
200
270
 
201
271
  return undefined;
202
272
  }
273
+
274
+ private normalizeUrl(url: string): string | undefined {
275
+ try {
276
+ return new URL(url).toString();
277
+ } catch {
278
+ return undefined;
279
+ }
280
+ }
281
+
282
+ private getUniqueCookies(url: string): Cookie[] {
283
+ const cookieUrl = this.normalizeUrl(url);
284
+ if (!cookieUrl) return [];
285
+
286
+ // tough-cookie returns longer (more-specific) paths first. Keeping the
287
+ // first cookie for each name prevents ambiguous duplicate-name headers.
288
+ const names = new Set<string>();
289
+ return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
290
+ if (names.has(cookie.key)) return false;
291
+ names.add(cookie.key);
292
+ return true;
293
+ });
294
+ }
203
295
  }
204
296
 
205
297
  function closestImpitBrowser(
@@ -280,12 +372,12 @@ function hasOwn(object: object, key: string): boolean {
280
372
  }
281
373
  function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
282
374
  return {
283
- setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
284
- cookieJar.setFromCookieStrings([cookie]);
375
+ setCookie(cookie: string, url: string, cb?: (error?: unknown) => void) {
376
+ cookieJar.setFromCookieStrings([cookie], url);
285
377
  if (typeof cb === "function") cb();
286
378
  },
287
- getCookieString(_url: string) {
288
- return cookieJar.toString();
379
+ getCookieString(url: string) {
380
+ return cookieJar.toHeader(url);
289
381
  },
290
382
  };
291
383
  }
@@ -337,10 +429,17 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
337
429
  export async function normalizeResponse(
338
430
  response: StealthTransportResponse,
339
431
  requestUrl?: string,
432
+ maxBodyBytes?: number,
340
433
  ): Promise<StealthResponse> {
341
434
  const headers = Object.fromEntries(response.headers.entries());
342
- const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
343
- const bodyBytes = await response.arrayBuffer();
435
+ const cookies = new CookieJarImpl(
436
+ setCookieHeadersFromResponse(response.headers),
437
+ response.url ?? requestUrl,
438
+ );
439
+ const bodyBytes =
440
+ maxBodyBytes === undefined
441
+ ? await response.arrayBuffer()
442
+ : await readResponseBodyWithLimit(response, maxBodyBytes);
344
443
  const body = new TextDecoder().decode(bodyBytes);
345
444
 
346
445
  return {
@@ -368,6 +467,83 @@ export async function normalizeResponse(
368
467
  };
369
468
  }
370
469
 
470
+ function responseTooLargeError(maxBodyBytes: number, observedBytes: number): TransportError {
471
+ return new TransportError(
472
+ `Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`,
473
+ {
474
+ code: "response_too_large",
475
+ category: "upstream_http",
476
+ retryable: false,
477
+ status: 0,
478
+ },
479
+ );
480
+ }
481
+
482
+ function declaredContentLength(headers: Headers): number | undefined {
483
+ const contentLength = headers.get("content-length")?.trim();
484
+ if (!contentLength || !/^\d+$/.test(contentLength)) return undefined;
485
+ const parsed = Number(contentLength);
486
+ return Number.isFinite(parsed) ? parsed : undefined;
487
+ }
488
+
489
+ function abortTransportResponse(response: StealthTransportResponse): boolean {
490
+ if (!response.abort) return false;
491
+ try {
492
+ response.abort();
493
+ } catch {
494
+ // The size error remains the primary failure if impit has already closed the response.
495
+ }
496
+ return true;
497
+ }
498
+
499
+ async function readResponseBodyWithLimit(
500
+ response: StealthTransportResponse,
501
+ maxBodyBytes: number,
502
+ ): Promise<ArrayBuffer> {
503
+ const contentLength = declaredContentLength(response.headers);
504
+ if (contentLength !== undefined && contentLength > maxBodyBytes) {
505
+ if (!abortTransportResponse(response)) {
506
+ await response.body?.cancel().catch(() => undefined);
507
+ }
508
+ throw responseTooLargeError(maxBodyBytes, contentLength);
509
+ }
510
+
511
+ if (!response.body) {
512
+ throw new TransportError("Response body stream is unavailable", {
513
+ code: "transport_stream_unavailable",
514
+ category: "upstream_http",
515
+ status: 0,
516
+ });
517
+ }
518
+
519
+ const reader = response.body.getReader();
520
+ const chunks: Uint8Array[] = [];
521
+ let receivedBytes = 0;
522
+ try {
523
+ while (true) {
524
+ const { done, value } = await reader.read();
525
+ if (done) break;
526
+ receivedBytes += value.byteLength;
527
+ if (receivedBytes > maxBodyBytes) {
528
+ await reader.cancel().catch(() => undefined);
529
+ abortTransportResponse(response);
530
+ throw responseTooLargeError(maxBodyBytes, receivedBytes);
531
+ }
532
+ chunks.push(value);
533
+ }
534
+ } finally {
535
+ reader.releaseLock();
536
+ }
537
+
538
+ const bodyBytes = new Uint8Array(receivedBytes);
539
+ let offset = 0;
540
+ for (const chunk of chunks) {
541
+ bodyBytes.set(chunk, offset);
542
+ offset += chunk.byteLength;
543
+ }
544
+ return bodyBytes.buffer;
545
+ }
546
+
371
547
  function normalizeBody(body: StealthFetchOptions["body"]): string {
372
548
  if (body === undefined) {
373
549
  return "";
@@ -426,6 +602,7 @@ type ResolvedAttemptProxy = {
426
602
  url?: string;
427
603
  poolIndex?: number;
428
604
  proxyHash?: string;
605
+ vendor?: ProxyVendorName;
429
606
  };
430
607
 
431
608
  function proxyPoolIndexFromDiagnostics(
@@ -559,16 +736,6 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
559
736
  }
560
737
  }
561
738
 
562
- function isRedirectStatus(status: number): boolean {
563
- return [301, 302, 303, 307, 308].includes(status);
564
- }
565
-
566
- function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
567
- if (status === 303 && method !== "HEAD") return "GET";
568
- if ((status === 301 || status === 302) && method === "POST") return "GET";
569
- return method;
570
- }
571
-
572
739
  function locationHeader(headers: Record<string, string>): string | undefined {
573
740
  for (const [name, value] of Object.entries(headers)) {
574
741
  if (name.toLowerCase() === "location") return value;
@@ -585,7 +752,7 @@ function createSessionFetcher(
585
752
  let closed = false;
586
753
  let hasWarnedMissingProxy = false;
587
754
  const warn = clientOptions.warn ?? console.warn;
588
- const cookieJar = new CookieJarImpl([]);
755
+ const cookieJar = new CookieJarImpl([], baseUrl);
589
756
  const impitCookieJar = toImpitCookieJar(cookieJar);
590
757
 
591
758
  function getClient(
@@ -615,6 +782,7 @@ function createSessionFetcher(
615
782
  async function resolveRequestProxy(
616
783
  options?: StealthFetchOptions,
617
784
  proxyAttempt?: number,
785
+ refreshEpoch?: number,
618
786
  ): Promise<ResolvedAttemptProxy> {
619
787
  const resolvedProxy = await resolveProxyConfigAsync({
620
788
  proxy: options?.proxy ?? clientOptions.proxy,
@@ -626,6 +794,10 @@ function createSessionFetcher(
626
794
  proxyAttemptOffset: options?.proxyAttemptOffset,
627
795
  retryAttemptOffset: proxyAttempt,
628
796
  }),
797
+ // The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
798
+ // preserving the client TLS fingerprint end-to-end.
799
+ transportProtocols: ["http", "socks5"],
800
+ ...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
629
801
  telemetry: clientOptions.telemetry,
630
802
  });
631
803
 
@@ -638,42 +810,65 @@ function createSessionFetcher(
638
810
  url: resolvedProxy.url,
639
811
  poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
640
812
  proxyHash: proxyEndpointHash(resolvedProxy.url),
813
+ vendor: vendorFromResolvedSource(resolvedProxy.source),
641
814
  };
642
815
  }
643
816
 
644
817
  const session: StealthSession = {
645
818
  async fetch(url, options: StealthFetchOptions = {}) {
646
- const method = normalizeMethod(options.method ?? "GET");
647
- const hasExplicitRetryPolicy = options.retry !== undefined;
648
- const stealthRetryOptions =
649
- normalizeProxyTransportRetryOptions(options.retry, {
650
- extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
651
- label: "Stealth",
652
- }) ??
653
- (hasExplicitRetryPolicy
654
- ? undefined
655
- : createDefaultProxyTransportRetryOptions({
819
+ const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
820
+ try {
821
+ const method = normalizeMethod(options.method ?? "GET");
822
+ const hasExplicitRetryPolicy = options.retry !== undefined;
823
+ const stealthRetryOptions =
824
+ normalizeProxyTransportRetryOptions(options.retry, {
656
825
  extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
657
826
  label: "Stealth",
658
- }));
659
- if (stealthRetryOptions) {
660
- validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
661
- }
827
+ }) ??
828
+ (hasExplicitRetryPolicy
829
+ ? undefined
830
+ : createDefaultProxyTransportRetryOptions({
831
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
832
+ label: "Stealth",
833
+ }));
834
+ if (stealthRetryOptions) {
835
+ validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
836
+ }
837
+ return { hasExplicitRetryPolicy, method, stealthRetryOptions };
838
+ } catch (error) {
839
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
840
+ }
841
+ })();
662
842
  const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
663
843
  const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
664
844
  const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
845
+ // Span the whole vendor chain: successive attempts rotate one vendor's
846
+ // pool, then fail over to the next vendor via the flat attempt index.
847
+ const policyProxy =
848
+ clientOptions.proxyPolicy ??
849
+ (typeof clientOptions.upstream?.proxy === "object"
850
+ ? clientOptions.upstream.proxy
851
+ : undefined);
852
+ // The pool span is already bounded by each vendor's max pool size
853
+ // (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
854
+ // the chain's true maximum — a large NodeMaven pool stays fully
855
+ // reachable rather than being truncated at an arbitrary ceiling.
665
856
  const policyProxyAttemptCap = Math.max(
666
857
  1,
667
- Math.min(
668
- MAX_POLICY_PROXY_RETRY_ATTEMPTS,
669
- clientOptions.proxyPolicy?.session?.poolSize ??
670
- (typeof clientOptions.upstream?.proxy === "object"
671
- ? clientOptions.upstream.proxy.session?.poolSize
672
- : undefined) ??
673
- DEFAULT_SMARTPROXY_POOL_SIZE,
674
- ),
858
+ policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
675
859
  );
676
- const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
860
+ // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
861
+ // successive attempts resolve a *different* endpoint, so it is the only one
862
+ // that may widen the attempt cap to the pool span, de-duplicate endpoints,
863
+ // and drive allocator stale-pool refresh. A static custom/decodo policy
864
+ // resolves the same URL every attempt: widening/refreshing it would resend
865
+ // the request dozens of times (up to maxAttempts × refreshes) and bypass
866
+ // retry:false and unsafe-method controls. Static policies therefore follow
867
+ // the ordinary transport-retry budget instead.
868
+ const rotatesRegistryChain =
869
+ usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
870
+ const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
871
+ const dedupeAllocatorEndpoints = rotatesRegistryChain;
677
872
  let lastError: unknown;
678
873
 
679
874
  for (
@@ -688,6 +883,11 @@ function createSessionFetcher(
688
883
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
689
884
  let proxy: string | undefined;
690
885
  let attemptProxy: ResolvedAttemptProxy | undefined;
886
+ // Reuse the exact serialization used by this outbound attempt in its catch path.
887
+ let serializedUrl: ReturnType<typeof serializeRequestUrl> | undefined;
888
+ let fallbackSensitiveValues: readonly string[] = [];
889
+ let fallbackRequestUrl: string | undefined;
890
+ let fallbackRedactedUrl: string | undefined;
691
891
  const attemptStartedAt = Date.now();
692
892
  let attemptRecorded = false;
693
893
  const recordProxyAttempt = (
@@ -698,7 +898,7 @@ function createSessionFetcher(
698
898
  if (attemptRecorded || !proxy) return;
699
899
  attemptRecorded = true;
700
900
  clientOptions.telemetry?.recordProxyAttempt?.({
701
- provider: "smartproxy",
901
+ provider: attemptProxy?.vendor ?? "smartproxy",
702
902
  attempt: attempt + 1,
703
903
  ...(attemptProxy?.poolIndex === undefined
704
904
  ? {}
@@ -711,12 +911,27 @@ function createSessionFetcher(
711
911
  });
712
912
  };
713
913
  try {
914
+ const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
915
+ const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
916
+ fallbackSensitiveValues = [
917
+ ...new Set([
918
+ ...Object.values(sensitiveParams ?? {}).map(String),
919
+ ...structural.sensitiveValues,
920
+ ]),
921
+ ].filter((value) => value !== "");
922
+ fallbackRequestUrl = url;
923
+ fallbackRedactedUrl = structural.redactedUrl;
714
924
  assertNoUnsupportedFingerprintOverrides(options);
715
- attemptProxy = await resolveRequestProxy(options, attempt);
925
+ attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
716
926
  proxy = attemptProxy.url;
717
- if (proxy && usesPolicyAllocator) {
927
+ if (proxy && dedupeAllocatorEndpoints) {
928
+ // An under-filled allocation repeats endpoints (via the modulo
929
+ // pool mapping) before the flat offset crosses into the next
930
+ // vendor. Skip an already-tried endpoint and advance the offset
931
+ // rather than breaking — breaking here would strand the request on
932
+ // the primary vendor and never reach the fallback leg.
718
933
  if (attemptedProxies.has(proxy)) {
719
- break;
934
+ continue;
720
935
  }
721
936
  attemptedProxies.add(proxy);
722
937
  }
@@ -725,10 +940,15 @@ function createSessionFetcher(
725
940
  (!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify),
726
941
  );
727
942
  const profileName = options.profile ?? defaultProfile;
728
- const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
943
+ serializedUrl = serializeRequestUrl(
944
+ resolveUrl(baseUrl, url),
945
+ options.params,
946
+ sensitiveParams,
947
+ );
948
+ const { requestUrl } = serializedUrl;
729
949
  const headers = { ...(options.headers ?? {}) };
730
950
  if (!hasHeader(headers, "Cookie")) {
731
- const cookieHeader = cookieJar.toString();
951
+ const cookieHeader = cookieJar.toHeader(requestUrl);
732
952
  if (cookieHeader) headers.Cookie = cookieHeader;
733
953
  }
734
954
  const requestInit: StealthRequestInit = {
@@ -744,8 +964,11 @@ function createSessionFetcher(
744
964
  requestUrl,
745
965
  requestInit,
746
966
  );
747
- const normalized = await normalizeResponse(response, requestUrl);
748
- cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
967
+ const normalized = await normalizeResponse(response, requestUrl, options.maxBodyBytes);
968
+ cookieJar.setFromCookieStrings(
969
+ setCookieHeadersFromResponse(response.headers),
970
+ response.url ?? requestUrl,
971
+ );
749
972
 
750
973
  if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
751
974
  throw createProxyConnectFailureError(normalized.body);
@@ -789,16 +1012,36 @@ function createSessionFetcher(
789
1012
  recordProxyAttempt("ok", undefined, response.status);
790
1013
  return normalized;
791
1014
  } catch (error) {
792
- const normalizedError = normalizeStealthTransportError(error);
1015
+ const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
1016
+ let normalizedError: TransportError;
1017
+ try {
1018
+ normalizedError = normalizeStealthTransportError(error);
1019
+ } catch (normalizationError) {
1020
+ throw redactSensitiveError(
1021
+ normalizationError,
1022
+ sensitiveValues,
1023
+ serializedUrl?.requestUrl ?? fallbackRequestUrl,
1024
+ serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
1025
+ );
1026
+ }
1027
+ const retryErrorCode = proxyAttemptErrorCode(normalizedError);
1028
+ const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
1029
+ const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
1030
+ normalizedError = redactSensitiveError(
1031
+ normalizedError,
1032
+ sensitiveValues,
1033
+ serializedUrl?.requestUrl ?? fallbackRequestUrl,
1034
+ serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
1035
+ );
793
1036
  recordProxyAttempt(
794
1037
  "error",
795
1038
  proxyAttemptErrorCode(normalizedError),
796
1039
  proxyAttemptStatus(normalizedError),
797
1040
  );
798
1041
  lastError = normalizedError;
799
- if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
1042
+ if (proxy && rotatesRegistryChain && refreshableProxyError) {
800
1043
  stalePoolError = normalizedError;
801
- if (shouldRunProxyAuthDiagnostic(normalizedError)) {
1044
+ if (runProxyAuthDiagnostic) {
802
1045
  stalePoolDiagnosticProxy = proxy;
803
1046
  }
804
1047
  if (attempt + 1 < maxAttempts) {
@@ -806,13 +1049,30 @@ function createSessionFetcher(
806
1049
  }
807
1050
  break;
808
1051
  }
1052
+ // Cap the number of transport retries. For a policy-allocator chain,
1053
+ // every attempt resolves a *different* endpoint/vendor (poolIndex
1054
+ // rotates across the concatenated vendor pool spans), so a transport
1055
+ // failure is a signal to advance to the next endpoint — potentially
1056
+ // crossing into the fallback vendor — not to retry the same endpoint.
1057
+ // Truncating that rotation at the per-endpoint retry budget would
1058
+ // strand the request on the primary vendor and never reach the
1059
+ // fallback, since the crossover only happens once the flat attempt
1060
+ // index exceeds the primary vendor's pool size (~10-20).
1061
+ // resolvePolicyTransportAttemptCap widens to the full chain span only
1062
+ // for implicit, safe-method allocator requests; explicit retry
1063
+ // policies (their documented `attempts` ceiling), unsafe methods, and
1064
+ // static/non-registry vendors keep the per-endpoint retry budget.
1065
+ const transportRetryCap = resolvePolicyTransportAttemptCap({
1066
+ policy: policyProxy,
1067
+ usesPolicyAllocator,
1068
+ retryAttempts: stealthRetryOptions?.attempts ?? 1,
1069
+ explicitRetry: hasExplicitRetryPolicy,
1070
+ method,
1071
+ });
809
1072
  if (
810
- attempt + 1 <
811
- (stealthRetryOptions
812
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
813
- : maxAttempts) &&
1073
+ attempt + 1 < transportRetryCap &&
814
1074
  shouldRetryProxyTransportAttempt({
815
- error: normalizedError,
1075
+ error: { code: retryErrorCode },
816
1076
  explicitRetry: hasExplicitRetryPolicy,
817
1077
  method,
818
1078
  options: stealthRetryOptions,
@@ -829,7 +1089,7 @@ function createSessionFetcher(
829
1089
  }
830
1090
 
831
1091
  if (
832
- usesPolicyAllocator &&
1092
+ rotatesRegistryChain &&
833
1093
  stalePoolError &&
834
1094
  refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
835
1095
  ) {
@@ -882,25 +1142,99 @@ function createSessionFetcher(
882
1142
  options.maxHops === undefined || !Number.isFinite(options.maxHops)
883
1143
  ? 10
884
1144
  : Math.max(0, Math.floor(options.maxHops));
1145
+ const {
1146
+ url: _url,
1147
+ maxHops: _maxHops,
1148
+ stopWhen,
1149
+ params,
1150
+ sensitiveParams,
1151
+ ...fetchOptions
1152
+ } = options;
885
1153
  const hops: StealthRedirectHop[] = [];
886
- let currentUrl = resolveUrl(baseUrl, options.url);
887
1154
  let method = normalizeMethod(options.method ?? "GET");
888
1155
  let body = options.body;
889
1156
  let response: StealthResponse | undefined;
890
1157
  const visitedRequests = new Set<string>();
891
-
892
- const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
1158
+ const initialParams = params
1159
+ ? Object.fromEntries(
1160
+ Object.entries(params).map(([key, value]) => [
1161
+ key,
1162
+ Array.isArray(value) ? [...value] : value,
1163
+ ]),
1164
+ )
1165
+ : undefined;
1166
+ const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
1167
+ const initialSensitiveParams = normalizedSensitiveParams
1168
+ ? { ...normalizedSensitiveParams }
1169
+ : undefined;
1170
+ const sensitiveParamNames = initialSensitiveParams
1171
+ ? Object.keys(initialSensitiveParams)
1172
+ : [];
1173
+ const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
1174
+ const sensitiveValues = new Set(
1175
+ [
1176
+ ...Object.values(initialSensitiveParams ?? {}),
1177
+ ...callerStructural.sensitiveValues,
1178
+ ].filter((value) => value !== ""),
1179
+ );
1180
+ const redactRedirectUrl = (value: string): string => {
1181
+ const structural = redactUrlQueryParams(value, [
1182
+ ...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
1183
+ ]);
1184
+ for (const sensitiveValue of structural.sensitiveValues) {
1185
+ sensitiveValues.add(sensitiveValue);
1186
+ }
1187
+ return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
1188
+ };
1189
+ let currentUrl: string;
1190
+ let initialUrl: ReturnType<typeof serializeRequestUrl>;
1191
+ try {
1192
+ currentUrl = resolveUrl(baseUrl, options.url);
1193
+ redactRedirectUrl(currentUrl);
1194
+ initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
1195
+ for (const value of initialUrl.sensitiveValues) {
1196
+ if (value !== "") sensitiveValues.add(value);
1197
+ }
1198
+ } catch (error) {
1199
+ throw redactSensitiveError(
1200
+ error,
1201
+ [...sensitiveValues],
1202
+ options.url,
1203
+ redactRedirectUrl(options.url),
1204
+ );
1205
+ }
893
1206
 
894
1207
  for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
895
- visitedRequests.add(`${method} ${currentUrl}`);
896
- response = await session.fetch(currentUrl, {
897
- ...fetchOptions,
898
- body,
899
- method,
900
- ...(hopIndex === 0 && params ? { params } : {}),
901
- redirect: "manual",
902
- throwOnHttpError: false,
903
- });
1208
+ const outboundUrl =
1209
+ hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
1210
+ // Preserve params-only loop bookkeeping from before sensitiveParams:
1211
+ // the first visited key is the caller's resolved URL, not its expanded query.
1212
+ const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
1213
+ visitedRequests.add(`${method} ${visitedUrl}`);
1214
+ try {
1215
+ response = await session.fetch(currentUrl, {
1216
+ ...fetchOptions,
1217
+ body,
1218
+ method,
1219
+ ...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
1220
+ ...(hopIndex === 0 && initialSensitiveParams
1221
+ ? { sensitiveParams: initialSensitiveParams }
1222
+ : {}),
1223
+ redirect: "manual",
1224
+ throwOnHttpError: false,
1225
+ });
1226
+ } catch (error) {
1227
+ throw redactSensitiveError(
1228
+ error,
1229
+ [...sensitiveValues],
1230
+ outboundUrl,
1231
+ redactRedirectUrl(outboundUrl),
1232
+ );
1233
+ }
1234
+ // StealthResponse.url is programmatic metadata and remains raw. Only the
1235
+ // redirect hop emitted below is a diagnostic surface.
1236
+ const responseUrl =
1237
+ response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
904
1238
 
905
1239
  if (!isRedirectStatus(response.status)) {
906
1240
  return {
@@ -908,63 +1242,79 @@ function createSessionFetcher(
908
1242
  hops,
909
1243
  reason: "completed",
910
1244
  cookies: cookieJar.snapshot(),
1245
+ cookieStore: cookieJar.serialize(),
911
1246
  };
912
1247
  }
913
1248
 
914
1249
  const location = locationHeader(response.headers);
915
- const nextUrl = location
916
- ? new URL(location, response.url ?? currentUrl).toString()
917
- : undefined;
918
- const hop: StealthRedirectHop = {
919
- url: response.url ?? currentUrl,
1250
+ const redactedResponseUrl = redactRedirectUrl(responseUrl);
1251
+ const redactedLocation = location ? redactRedirectUrl(location) : undefined;
1252
+ let nextUrl: string | undefined;
1253
+ try {
1254
+ nextUrl = resolveRedirectUrl(location, responseUrl);
1255
+ } catch (error) {
1256
+ throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
1257
+ }
1258
+ const realHop: StealthRedirectHop = {
1259
+ url: responseUrl,
920
1260
  status: response.status,
921
1261
  method,
922
1262
  ...(location ? { location } : {}),
923
1263
  ...(nextUrl ? { nextUrl } : {}),
924
1264
  };
1265
+ const hop: StealthRedirectHop = {
1266
+ ...realHop,
1267
+ url: redactedResponseUrl,
1268
+ ...(redactedLocation ? { location: redactedLocation } : {}),
1269
+ ...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
1270
+ };
925
1271
  hops.push(hop);
926
1272
 
927
- if (stopWhen && (await stopWhen(hop))) {
928
- return {
929
- final: response,
930
- hops,
931
- reason: "stopped",
932
- cookies: cookieJar.snapshot(),
933
- };
934
- }
935
-
936
- if (!nextUrl) {
937
- return {
938
- final: response,
939
- hops,
940
- reason: "missing_location",
941
- cookies: cookieJar.snapshot(),
942
- };
1273
+ let shouldStop = false;
1274
+ if (stopWhen) {
1275
+ try {
1276
+ shouldStop = await stopWhen(realHop);
1277
+ } catch (error) {
1278
+ let sanitizedError: unknown = error;
1279
+ for (const [rawUrl, safeUrl] of [
1280
+ [responseUrl, redactedResponseUrl],
1281
+ [location, redactedLocation],
1282
+ [nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
1283
+ ] as const) {
1284
+ if (!rawUrl || !safeUrl) continue;
1285
+ sanitizedError = redactSensitiveError(
1286
+ sanitizedError,
1287
+ [...sensitiveValues],
1288
+ rawUrl,
1289
+ safeUrl,
1290
+ );
1291
+ }
1292
+ throw sanitizedError;
1293
+ }
943
1294
  }
944
-
945
- if (hops.length > maxHops) {
1295
+ const decision = evaluateRedirectHop({
1296
+ status: response.status,
1297
+ method,
1298
+ nextUrl,
1299
+ shouldStop,
1300
+ redirectCount: hops.length,
1301
+ maxHops,
1302
+ visitedRequests,
1303
+ });
1304
+ if (decision.kind === "stop") {
946
1305
  return {
947
1306
  final: response,
948
1307
  hops,
949
- reason: "max_hops",
1308
+ reason: decision.reason,
950
1309
  cookies: cookieJar.snapshot(),
1310
+ cookieStore: cookieJar.serialize(),
951
1311
  };
952
1312
  }
953
-
954
- const nextMethod = nextRedirectMethod(response.status, method);
955
- if (nextMethod !== method) {
1313
+ if (decision.nextMethod !== method) {
956
1314
  body = undefined;
957
1315
  }
958
- if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
959
- return {
960
- final: response,
961
- hops,
962
- reason: "loop",
963
- cookies: cookieJar.snapshot(),
964
- };
965
- }
966
- method = nextMethod;
967
- currentUrl = nextUrl;
1316
+ method = decision.nextMethod;
1317
+ currentUrl = decision.nextUrl;
968
1318
  }
969
1319
 
970
1320
  if (!response) {
@@ -982,6 +1332,7 @@ function createSessionFetcher(
982
1332
  hops,
983
1333
  reason: "max_hops",
984
1334
  cookies: cookieJar.snapshot(),
1335
+ cookieStore: cookieJar.serialize(),
985
1336
  };
986
1337
  },
987
1338
  },