@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.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 (231) hide show
  1. package/AUTHORING.md +330 -8
  2. package/CHANGELOG.md +89 -1
  3. package/README.md +64 -17
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +58 -8
  7. package/bin/apifuse-pack-check.ts +32 -2
  8. package/bin/apifuse-pack-smoke.ts +133 -6
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +3243 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/auth.d.ts +76 -0
  14. package/dist/auth.js +436 -0
  15. package/dist/ceremonies/index.d.ts +41 -0
  16. package/dist/ceremonies/index.js +490 -0
  17. package/dist/choice-token.d.ts +24 -0
  18. package/dist/choice-token.js +74 -0
  19. package/dist/cli/commands.d.ts +10 -0
  20. package/dist/cli/commands.js +80 -0
  21. package/dist/cli/create.d.ts +47 -0
  22. package/dist/cli/create.js +777 -0
  23. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  24. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  25. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  26. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  27. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  28. package/dist/cli/templates/provider/README.md.tpl +163 -0
  29. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  30. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  31. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  32. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  33. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  34. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  35. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  36. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  37. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  38. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  39. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  40. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  41. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  42. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  43. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  44. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  46. package/dist/config/loader.d.ts +107 -0
  47. package/dist/config/loader.js +935 -0
  48. package/dist/contract-json.d.ts +9 -0
  49. package/dist/contract-json.js +51 -0
  50. package/dist/contract-serialization.d.ts +4 -0
  51. package/dist/contract-serialization.js +78 -0
  52. package/dist/contract-types.d.ts +49 -0
  53. package/dist/contract-types.js +1 -0
  54. package/dist/contract.d.ts +6 -0
  55. package/dist/contract.js +156 -0
  56. package/dist/define.d.ts +100 -0
  57. package/dist/define.js +1383 -0
  58. package/dist/dev.d.ts +9 -0
  59. package/dist/dev.js +15 -0
  60. package/dist/errors.d.ts +59 -0
  61. package/dist/errors.js +97 -0
  62. package/dist/i18n/catalog.d.ts +29 -0
  63. package/dist/i18n/catalog.js +159 -0
  64. package/dist/i18n/index.d.ts +2 -0
  65. package/dist/i18n/index.js +2 -0
  66. package/dist/i18n/keys.d.ts +10 -0
  67. package/dist/i18n/keys.js +34 -0
  68. package/dist/index.d.ts +42 -0
  69. package/dist/index.js +38 -0
  70. package/dist/lint.d.ts +74 -0
  71. package/dist/lint.js +729 -0
  72. package/dist/observability.d.ts +5 -0
  73. package/dist/observability.js +39 -0
  74. package/dist/provider.d.ts +11 -0
  75. package/dist/provider.js +9 -0
  76. package/dist/public-schema-field-lint.d.ts +2 -0
  77. package/dist/public-schema-field-lint.js +158 -0
  78. package/dist/recipes/gov-api.d.ts +19 -0
  79. package/dist/recipes/gov-api.js +72 -0
  80. package/dist/recipes/rest-api.d.ts +21 -0
  81. package/dist/recipes/rest-api.js +115 -0
  82. package/dist/runtime/auth-flow.d.ts +14 -0
  83. package/dist/runtime/auth-flow.js +46 -0
  84. package/dist/runtime/browser.d.ts +25 -0
  85. package/dist/runtime/browser.js +1237 -0
  86. package/dist/runtime/cache.d.ts +10 -0
  87. package/dist/runtime/cache.js +372 -0
  88. package/dist/runtime/choice.d.ts +15 -0
  89. package/dist/runtime/choice.js +435 -0
  90. package/dist/runtime/credential.d.ts +8 -0
  91. package/dist/runtime/credential.js +61 -0
  92. package/dist/runtime/env.d.ts +2 -0
  93. package/dist/runtime/env.js +10 -0
  94. package/dist/runtime/executor.d.ts +16 -0
  95. package/dist/runtime/executor.js +51 -0
  96. package/dist/runtime/http.d.ts +8 -0
  97. package/dist/runtime/http.js +726 -0
  98. package/dist/runtime/insights.d.ts +9 -0
  99. package/dist/runtime/insights.js +324 -0
  100. package/dist/runtime/instrumentation.d.ts +8 -0
  101. package/dist/runtime/instrumentation.js +269 -0
  102. package/dist/runtime/key-derivation.d.ts +24 -0
  103. package/dist/runtime/key-derivation.js +73 -0
  104. package/dist/runtime/keyring.d.ts +25 -0
  105. package/dist/runtime/keyring.js +93 -0
  106. package/dist/runtime/namespace.d.ts +9 -0
  107. package/dist/runtime/namespace.js +19 -0
  108. package/dist/runtime/otlp.d.ts +39 -0
  109. package/dist/runtime/otlp.js +103 -0
  110. package/dist/runtime/perf.d.ts +12 -0
  111. package/dist/runtime/perf.js +52 -0
  112. package/dist/runtime/prevalidate.d.ts +12 -0
  113. package/dist/runtime/prevalidate.js +173 -0
  114. package/dist/runtime/provider.d.ts +2 -0
  115. package/dist/runtime/provider.js +11 -0
  116. package/dist/runtime/proxy-errors.d.ts +21 -0
  117. package/dist/runtime/proxy-errors.js +83 -0
  118. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  119. package/dist/runtime/proxy-telemetry.js +174 -0
  120. package/dist/runtime/redis.d.ts +17 -0
  121. package/dist/runtime/redis.js +82 -0
  122. package/dist/runtime/request-options.d.ts +3 -0
  123. package/dist/runtime/request-options.js +42 -0
  124. package/dist/runtime/state.d.ts +17 -0
  125. package/dist/runtime/state.js +344 -0
  126. package/dist/runtime/stealth.d.ts +21 -0
  127. package/dist/runtime/stealth.js +980 -0
  128. package/dist/runtime/stt.d.ts +22 -0
  129. package/dist/runtime/stt.js +480 -0
  130. package/dist/runtime/trace.d.ts +26 -0
  131. package/dist/runtime/trace.js +142 -0
  132. package/dist/runtime/waterfall.d.ts +12 -0
  133. package/dist/runtime/waterfall.js +147 -0
  134. package/dist/schema.d.ts +74 -0
  135. package/dist/schema.js +243 -0
  136. package/dist/serve.d.ts +1 -0
  137. package/dist/serve.js +1 -0
  138. package/dist/server/index.d.ts +3 -0
  139. package/dist/server/index.js +2 -0
  140. package/dist/server/serve.d.ts +64 -0
  141. package/dist/server/serve.js +1118 -0
  142. package/dist/server/types.d.ts +136 -0
  143. package/dist/server/types.js +86 -0
  144. package/dist/stealth/profiles.d.ts +4 -0
  145. package/dist/stealth/profiles.js +259 -0
  146. package/dist/stream.d.ts +44 -0
  147. package/dist/stream.js +151 -0
  148. package/dist/testing/helpers.d.ts +23 -0
  149. package/dist/testing/helpers.js +95 -0
  150. package/dist/testing/index.d.ts +2 -0
  151. package/dist/testing/index.js +2 -0
  152. package/dist/testing/run.d.ts +34 -0
  153. package/dist/testing/run.js +307 -0
  154. package/dist/types.d.ts +1467 -0
  155. package/dist/types.js +61 -0
  156. package/dist/utils/date.d.ts +6 -0
  157. package/dist/utils/date.js +101 -0
  158. package/dist/utils/parse.d.ts +16 -0
  159. package/dist/utils/parse.js +51 -0
  160. package/dist/utils/text.d.ts +4 -0
  161. package/dist/utils/text.js +14 -0
  162. package/dist/utils/transform.d.ts +8 -0
  163. package/dist/utils/transform.js +48 -0
  164. package/package.json +57 -29
  165. package/src/auth.ts +786 -0
  166. package/src/ceremonies/index.ts +8 -2
  167. package/src/choice-token.ts +165 -0
  168. package/src/cli/commands.ts +34 -11
  169. package/src/cli/create.ts +254 -128
  170. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  171. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  173. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  174. package/src/cli/templates/provider/README.md.tpl +87 -7
  175. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  176. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  177. package/src/cli/templates/provider/index.ts.tpl +5 -47
  178. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  179. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  180. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  181. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  182. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  183. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  184. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  185. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  186. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  187. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  188. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  189. package/src/cli/templates/provider/start.ts.tpl +1 -1
  190. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  191. package/src/config/loader.ts +1224 -9
  192. package/src/contract-json.ts +75 -0
  193. package/src/contract-serialization.ts +89 -0
  194. package/src/contract-types.ts +52 -0
  195. package/src/contract.ts +216 -0
  196. package/src/define.ts +1820 -70
  197. package/src/errors.ts +27 -0
  198. package/src/i18n/catalog.ts +277 -0
  199. package/src/i18n/index.ts +2 -0
  200. package/src/i18n/keys.ts +64 -0
  201. package/src/index.ts +189 -9
  202. package/src/lint.ts +580 -73
  203. package/src/observability.ts +41 -0
  204. package/src/provider.ts +131 -4
  205. package/src/public-schema-field-lint.ts +237 -0
  206. package/src/runtime/auth-flow.ts +9 -0
  207. package/src/runtime/browser.ts +1054 -51
  208. package/src/runtime/cache.ts +528 -0
  209. package/src/runtime/choice.ts +760 -0
  210. package/src/runtime/executor.ts +32 -3
  211. package/src/runtime/http.ts +980 -195
  212. package/src/runtime/insights.ts +11 -11
  213. package/src/runtime/instrumentation.ts +12 -4
  214. package/src/runtime/key-derivation.ts +1 -1
  215. package/src/runtime/keyring.ts +4 -3
  216. package/src/runtime/proxy-errors.ts +132 -0
  217. package/src/runtime/proxy-telemetry.ts +253 -0
  218. package/src/runtime/redis.ts +116 -0
  219. package/src/runtime/request-options.ts +66 -0
  220. package/src/runtime/state.ts +563 -0
  221. package/src/runtime/stealth.ts +1336 -0
  222. package/src/runtime/stt.ts +629 -0
  223. package/src/runtime/trace.ts +1 -1
  224. package/src/schema.ts +363 -1
  225. package/src/server/serve.ts +1192 -75
  226. package/src/server/types.ts +37 -0
  227. package/src/stream.ts +210 -0
  228. package/src/testing/run.ts +40 -6
  229. package/src/types.ts +1283 -59
  230. package/src/runtime/tls.ts +0 -434
  231. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,1336 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
3
+ import { Impit } from "impit";
4
+
5
+ import type { ProxyResolutionOptions } from "../config/loader";
6
+ import {
7
+ DEFAULT_SMARTPROXY_POOL_SIZE,
8
+ invalidateProxyResolutionCacheAsync,
9
+ ProxyResolutionError,
10
+ resolveProxyConfigAsync,
11
+ SMARTPROXY_MAX_POOL_SIZE,
12
+ } from "../config/loader";
13
+ import { ProviderError, SDKError, TransportError } from "../errors";
14
+ import { getStealthProfile } from "../stealth/profiles";
15
+ import type {
16
+ CookieJar,
17
+ HttpMethod,
18
+ HttpRetryOptions,
19
+ StealthClient,
20
+ StealthFetchOptions,
21
+ StealthRedirectHop,
22
+ StealthResponse,
23
+ StealthSession,
24
+ } from "../types";
25
+ import { HttpRetryPreset, HttpRetryUnsafeMethodPolicy } from "../types";
26
+ import {
27
+ createProxyAuthIpDeniedError,
28
+ createProxyEdgeAuthRejectedError,
29
+ createProxyEdgeTlsRejectedError,
30
+ createProxyPoolExhaustedError,
31
+ createProxyPoolStaleError,
32
+ isProxyAuthIpDeniedMessage,
33
+ isProxyEdgeAuthRejectedMessage,
34
+ isProxyEdgeTlsRejectedResponse,
35
+ isProxyPoolRefreshableError,
36
+ isProxyPoolStaleMessage,
37
+ isProxyPoolStaleStatus,
38
+ PROXY_AUTH_IP_DENIED_CODE,
39
+ PROXY_EDGE_AUTH_REJECTED_CODE,
40
+ PROXY_POOL_STALE_CODE,
41
+ } from "./proxy-errors";
42
+ import { appendQueryParams } from "./request-options";
43
+
44
+ const DEFAULT_PROFILE = "chrome-146";
45
+
46
+ const MISSING_PROXY_WARNING =
47
+ "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
48
+
49
+ const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
50
+ const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
51
+ const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
52
+ const PROXY_CONNECT_FAILURE_BODY_PATTERN =
53
+ /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
54
+ const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
55
+ const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
56
+ const DEFAULT_STEALTH_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"] as const;
57
+ const DEFAULT_STEALTH_RETRY_ERROR_CODES = [
58
+ PROXY_CONNECT_FAILURE_CODE,
59
+ "transport_network_error",
60
+ "transport_timeout",
61
+ ] as const;
62
+ const RATE_LIMIT_STEALTH_RETRY_ERROR_CODES = ["transport_timeout"] as const;
63
+ const KNOWN_STEALTH_RETRY_METHODS = new Set([
64
+ "GET",
65
+ "HEAD",
66
+ "POST",
67
+ "PUT",
68
+ "DELETE",
69
+ "OPTIONS",
70
+ "TRACE",
71
+ "PATCH",
72
+ ]);
73
+ const UNSAFE_STEALTH_RETRY_METHODS = new Set([
74
+ "POST",
75
+ "PUT",
76
+ "PATCH",
77
+ "DELETE",
78
+ "TRACE",
79
+ ]);
80
+ const MAX_STEALTH_RETRY_ATTEMPTS = 8;
81
+
82
+ export type StealthClientOptions = ProxyResolutionOptions & {
83
+ warn?: (message: string) => void;
84
+ /**
85
+ * Proxy-only stealth transport overrides. Use only for upstream proxy products
86
+ * that terminate CONNECT with a private CA instead of tunneling the origin
87
+ * certificate chain.
88
+ */
89
+ proxyStealth?: { insecureSkipVerify?: boolean };
90
+ };
91
+
92
+ const REMOVED_CHROME_PROFILE_NAMES = new Set([
93
+ "chrome-120",
94
+ "chrome-124",
95
+ "chrome-129",
96
+ "chrome-130",
97
+ "chrome-131",
98
+ "chrome-133",
99
+ "chrome-144",
100
+ "chrome-146-psk",
101
+ "chrome-131-psk",
102
+ "chrome-130-psk",
103
+ "edge-131",
104
+ ]);
105
+
106
+ type ImpitBrowser = Browser;
107
+ type ImpitRequestInit = RequestInit;
108
+
109
+ const CHROME_IMPIT_BY_MAJOR: Record<number, ImpitBrowser> = {
110
+ 100: "chrome100",
111
+ 101: "chrome101",
112
+ 104: "chrome104",
113
+ 107: "chrome107",
114
+ 110: "chrome110",
115
+ 116: "chrome116",
116
+ 124: "chrome124",
117
+ 125: "chrome125",
118
+ 131: "chrome131",
119
+ 136: "chrome136",
120
+ 142: "chrome142",
121
+ };
122
+
123
+ const FIREFOX_IMPIT_BY_MAJOR: Record<number, ImpitBrowser> = {
124
+ 128: "firefox128",
125
+ 133: "firefox133",
126
+ 135: "firefox135",
127
+ 144: "firefox144",
128
+ };
129
+
130
+ type StealthTransportResponse = Pick<
131
+ ImpitResponse,
132
+ "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
133
+ > & {
134
+ url?: string;
135
+ redirected?: boolean;
136
+ };
137
+
138
+ type StealthMethod = NonNullable<ImpitRequestInit["method"]>;
139
+ type StealthRequestInit = ImpitRequestInit & {
140
+ redirect?: NonNullable<StealthFetchOptions["redirect"]>;
141
+ };
142
+ type NormalizedStealthRetryOptions = {
143
+ attempts: number;
144
+ methods: readonly string[];
145
+ errorCodes: readonly string[];
146
+ unsafeMethodPolicy: HttpRetryOptions["unsafeMethodPolicy"];
147
+ };
148
+
149
+ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
150
+ return typeof value === "object" && value !== null;
151
+ }
152
+
153
+ class CookieJarImpl implements CookieJar {
154
+ private readonly cookies: Record<string, string>;
155
+
156
+ constructor(cookieStrings: string[]) {
157
+ this.cookies = {};
158
+ this.setFromCookieStrings(cookieStrings);
159
+ }
160
+
161
+ setFromCookieStrings(cookieStrings: readonly string[]): void {
162
+ for (const cookieString of cookieStrings) {
163
+ const [nameValue] = cookieString.split(";");
164
+ if (!nameValue) {
165
+ continue;
166
+ }
167
+
168
+ const separatorIndex = nameValue.indexOf("=");
169
+ if (separatorIndex === -1) {
170
+ continue;
171
+ }
172
+
173
+ const name = nameValue.slice(0, separatorIndex).trim();
174
+ const value = nameValue.slice(separatorIndex + 1).trim();
175
+ if (name) this.cookies[name] = value;
176
+ }
177
+ }
178
+
179
+ get(name: string): string | undefined {
180
+ return this.cookies[name];
181
+ }
182
+
183
+ getAll(): Record<string, string> {
184
+ return { ...this.cookies };
185
+ }
186
+
187
+ has(name: string): boolean {
188
+ return Object.hasOwn(this.cookies, name);
189
+ }
190
+
191
+ toString(): string {
192
+ return Object.entries(this.cookies)
193
+ .map(([name, value]) => `${name}=${value}`)
194
+ .join("; ");
195
+ }
196
+
197
+ toHeader(): string {
198
+ return this.toString();
199
+ }
200
+
201
+ snapshot(): Record<string, string> {
202
+ return this.getAll();
203
+ }
204
+
205
+ restore(cookies: Record<string, string>): void {
206
+ this.clear();
207
+ for (const [name, value] of Object.entries(cookies)) {
208
+ if (name) this.cookies[name] = value;
209
+ }
210
+ }
211
+
212
+ clear(): void {
213
+ for (const name of Object.keys(this.cookies)) {
214
+ delete this.cookies[name];
215
+ }
216
+ }
217
+
218
+ find(predicate: (cookie: string) => boolean): string | undefined {
219
+ for (const [name, value] of Object.entries(this.cookies)) {
220
+ const cookie = `${name}=${value}`;
221
+ if (predicate(cookie)) {
222
+ return cookie;
223
+ }
224
+ }
225
+
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ function closestImpitBrowser(
231
+ major: number,
232
+ candidates: Record<number, ImpitBrowser>,
233
+ ): ImpitBrowser {
234
+ let closestMajor: number | undefined;
235
+ let closestBrowser: ImpitBrowser | undefined;
236
+ for (const [candidateMajorText, browser] of Object.entries(candidates)) {
237
+ const candidateMajor = Number(candidateMajorText);
238
+ if (
239
+ closestMajor === undefined ||
240
+ Math.abs(candidateMajor - major) < Math.abs(closestMajor - major)
241
+ ) {
242
+ closestMajor = candidateMajor;
243
+ closestBrowser = browser;
244
+ }
245
+ }
246
+ return closestBrowser ?? "chrome142";
247
+ }
248
+
249
+ function resolveImpitBrowser(profileName: string): ImpitBrowser {
250
+ if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
251
+ throw new SDKError(`Unknown stealth profile: ${profileName}`);
252
+ }
253
+
254
+ let profile: ReturnType<typeof getStealthProfile>;
255
+ try {
256
+ profile = getStealthProfile(profileName);
257
+ } catch {
258
+ // Preserve the previous ctx.stealth.fetch() compatibility behavior: unknown
259
+ // profile strings still run with the transport default instead of failing
260
+ // before the request starts. Removed built-in profile aliases above remain
261
+ // explicit errors so callers do not accidentally pin retired fingerprints.
262
+ return "chrome142";
263
+ }
264
+
265
+ const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
266
+ const chromeMatch = /^(?:chrome|edge)_(\d+)/.exec(identifier);
267
+ if (chromeMatch?.[1]) {
268
+ return closestImpitBrowser(Number(chromeMatch[1]), CHROME_IMPIT_BY_MAJOR);
269
+ }
270
+ const firefoxMatch = /^firefox_(\d+)/.exec(identifier);
271
+ if (firefoxMatch?.[1]) {
272
+ return closestImpitBrowser(Number(firefoxMatch[1]), FIREFOX_IMPIT_BY_MAJOR);
273
+ }
274
+ if (identifier.startsWith("safari_")) {
275
+ throw new SDKError(
276
+ `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.`,
277
+ );
278
+ }
279
+ throw new SDKError(
280
+ `Stealth profile "${profileName}" cannot be mapped to an impit browser profile.`,
281
+ );
282
+ }
283
+
284
+ function resolveUrl(baseUrl: string, url: string): string {
285
+ return new URL(url, baseUrl).toString();
286
+ }
287
+
288
+ function headerEntriesFromHeaders(headers: Headers): [string, string][] {
289
+ return Array.from(headers.entries());
290
+ }
291
+
292
+ function normalizeHeaders(
293
+ headers: Record<string, string | string[] | undefined>,
294
+ ): Record<string, string> {
295
+ const normalized: Record<string, string> = {};
296
+ for (const [name, value] of Object.entries(headers)) {
297
+ if (value === undefined) continue;
298
+ normalized[name] = Array.isArray(value) ? value.join(", ") : value;
299
+ }
300
+ return normalized;
301
+ }
302
+
303
+ function hasOwn(object: object, key: string): boolean {
304
+ return Object.hasOwn(object, key);
305
+ }
306
+ function toImpitCookieJar(
307
+ cookieJar: CookieJarImpl,
308
+ ): NonNullable<ImpitOptions["cookieJar"]> {
309
+ return {
310
+ setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
311
+ cookieJar.setFromCookieStrings([cookie]);
312
+ if (typeof cb === "function") cb();
313
+ },
314
+ getCookieString(_url: string) {
315
+ return cookieJar.toString();
316
+ },
317
+ };
318
+ }
319
+
320
+ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
321
+ if (!isRecord(options)) return;
322
+ const unsupported: string[] = [];
323
+ if (hasOwn(options, "headerOrder")) unsupported.push("headerOrder");
324
+ const stealth = options.stealth;
325
+ if (isRecord(stealth) && hasOwn(stealth, "ja3"))
326
+ unsupported.push("stealth.ja3");
327
+ if (isRecord(stealth) && hasOwn(stealth, "h2"))
328
+ unsupported.push("stealth.h2");
329
+ if (unsupported.length === 0) return;
330
+
331
+ throw new SDKError(
332
+ `ctx.stealth.fetch uses impit-managed browser fingerprints and no longer accepts low-level stealth overrides: ${unsupported.join(", ")}. Use the profile option instead.`,
333
+ );
334
+ }
335
+
336
+ function responseHeadersToRecord(
337
+ headers: Headers,
338
+ ): Record<string, string | string[] | undefined> {
339
+ const record: Record<string, string> = {};
340
+ for (const [name, value] of headers.entries()) record[name] = value;
341
+ return record;
342
+ }
343
+
344
+ function setCookieHeadersFromResponse(headers: Headers): string[] {
345
+ const getSetCookie = headers.getSetCookie;
346
+ if (typeof getSetCookie === "function") return getSetCookie.call(headers);
347
+ const setCookie = headers.get("set-cookie");
348
+ return setCookie ? splitCombinedSetCookieHeader(setCookie) : [];
349
+ }
350
+
351
+ function splitCombinedSetCookieHeader(headerValue: string): string[] {
352
+ const cookieNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+=/;
353
+ const cookieStrings: string[] = [];
354
+ let start = 0;
355
+ for (let index = 0; index < headerValue.length; index += 1) {
356
+ if (headerValue[index] !== ",") continue;
357
+ const next = headerValue.slice(index + 1).trimStart();
358
+ if (!cookieNamePattern.test(next)) continue;
359
+ const cookie = headerValue.slice(start, index).trim();
360
+ if (cookie) cookieStrings.push(cookie);
361
+ start = index + 1;
362
+ }
363
+ const finalCookie = headerValue.slice(start).trim();
364
+ if (finalCookie) cookieStrings.push(finalCookie);
365
+ return cookieStrings;
366
+ }
367
+
368
+ export async function normalizeResponse(
369
+ response: StealthTransportResponse,
370
+ requestUrl?: string,
371
+ ): Promise<StealthResponse> {
372
+ const headers = Object.fromEntries(response.headers.entries());
373
+ const cookies = new CookieJarImpl(
374
+ setCookieHeadersFromResponse(response.headers),
375
+ );
376
+ const bodyBytes = await response.arrayBuffer();
377
+ const body = new TextDecoder().decode(bodyBytes);
378
+
379
+ return {
380
+ status: response.status,
381
+ ok: response.status >= 200 && response.status < 300,
382
+ ...(response.url ? { url: response.url } : {}),
383
+ ...(response.redirected !== undefined
384
+ ? { redirected: response.redirected }
385
+ : requestUrl && response.url
386
+ ? { redirected: response.url !== requestUrl }
387
+ : {}),
388
+ headers,
389
+ rawHeaders: headerEntriesFromHeaders(response.headers),
390
+ body,
391
+ cookies,
392
+ json<T>(): Promise<T> {
393
+ return Promise.resolve(JSON.parse(body));
394
+ },
395
+ arrayBuffer(): Promise<ArrayBuffer> {
396
+ return Promise.resolve(bodyBytes.slice(0));
397
+ },
398
+ bytes(): Promise<Uint8Array> {
399
+ return Promise.resolve(new Uint8Array(bodyBytes.slice(0)));
400
+ },
401
+ };
402
+ }
403
+
404
+ function normalizeBody(body: StealthFetchOptions["body"]): string {
405
+ if (body === undefined) {
406
+ return "";
407
+ }
408
+
409
+ if (typeof body === "string") {
410
+ return body;
411
+ }
412
+
413
+ if (Buffer.isBuffer(body)) {
414
+ return body.toString();
415
+ }
416
+
417
+ return String(body);
418
+ }
419
+
420
+ function isPolicyManagedProxy(options: StealthClientOptions): boolean {
421
+ const policy = options.proxyPolicy ?? options.upstream?.proxy;
422
+ return Boolean(policy && typeof policy === "object");
423
+ }
424
+
425
+ function isRetrySafeStealthMethod(method: StealthMethod): boolean {
426
+ return method === "GET" || method === "HEAD" || method === "OPTIONS";
427
+ }
428
+
429
+ function createStealthRetryOptions(
430
+ preset: HttpRetryPreset,
431
+ ): NormalizedStealthRetryOptions {
432
+ switch (preset) {
433
+ case HttpRetryPreset.Off:
434
+ return {
435
+ attempts: 1,
436
+ methods: DEFAULT_STEALTH_RETRY_METHODS,
437
+ errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
438
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
439
+ };
440
+ case HttpRetryPreset.AggressiveRead:
441
+ return {
442
+ attempts: 4,
443
+ methods: DEFAULT_STEALTH_RETRY_METHODS,
444
+ errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
445
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
446
+ };
447
+ case HttpRetryPreset.RateLimitAware:
448
+ return {
449
+ attempts: 3,
450
+ methods: DEFAULT_STEALTH_RETRY_METHODS,
451
+ errorCodes: RATE_LIMIT_STEALTH_RETRY_ERROR_CODES,
452
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
453
+ };
454
+ case HttpRetryPreset.SafeRead:
455
+ case HttpRetryPreset.TransportTransient:
456
+ return {
457
+ attempts: 3,
458
+ methods: DEFAULT_STEALTH_RETRY_METHODS,
459
+ errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
460
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
461
+ };
462
+ }
463
+ throw new ProviderError(`Unknown stealth retry preset: ${preset}`, {
464
+ code: "retry_invalid_policy",
465
+ });
466
+ }
467
+
468
+ function normalizeStealthRetryOptions(
469
+ retry: StealthFetchOptions["retry"],
470
+ ): NormalizedStealthRetryOptions | undefined {
471
+ if (retry === undefined) return undefined;
472
+ if (retry === false) return createStealthRetryOptions(HttpRetryPreset.Off);
473
+ if (retry === true)
474
+ return createStealthRetryOptions(HttpRetryPreset.TransportTransient);
475
+ if (typeof retry === "string") {
476
+ if (!Object.values(HttpRetryPreset).includes(retry)) {
477
+ throw new ProviderError(`Unknown stealth retry preset: ${retry}`, {
478
+ code: "retry_invalid_policy",
479
+ });
480
+ }
481
+ return createStealthRetryOptions(retry);
482
+ }
483
+ if (typeof retry !== "object" || retry === null || Array.isArray(retry)) {
484
+ throw new ProviderError("Stealth retry policy must be a plain object", {
485
+ code: "retry_invalid_policy",
486
+ });
487
+ }
488
+ if (
489
+ retry.unsafeMethodPolicy !== undefined &&
490
+ !Object.values(HttpRetryUnsafeMethodPolicy).includes(
491
+ retry.unsafeMethodPolicy,
492
+ )
493
+ ) {
494
+ throw new ProviderError(
495
+ `Unknown stealth retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
496
+ { code: "retry_invalid_policy" },
497
+ );
498
+ }
499
+ if (retry.methods !== undefined) {
500
+ if (!Array.isArray(retry.methods)) {
501
+ throw new ProviderError("Stealth retry methods must be an array", {
502
+ code: "retry_invalid_policy",
503
+ });
504
+ }
505
+ const unknownMethods = retry.methods
506
+ .map((method) => (typeof method === "string" ? method.toUpperCase() : ""))
507
+ .filter((method) => !KNOWN_STEALTH_RETRY_METHODS.has(method));
508
+ if (unknownMethods.length > 0) {
509
+ throw new ProviderError(
510
+ `Unknown stealth retry method(s): ${unknownMethods.join(", ")}`,
511
+ { code: "retry_invalid_policy" },
512
+ );
513
+ }
514
+ }
515
+ if (retry.errorCodes !== undefined) {
516
+ if (
517
+ !Array.isArray(retry.errorCodes) ||
518
+ retry.errorCodes.some((errorCode) => typeof errorCode !== "string")
519
+ ) {
520
+ throw new ProviderError(
521
+ "Stealth retry errorCodes must contain only strings",
522
+ { code: "retry_invalid_policy" },
523
+ );
524
+ }
525
+ }
526
+
527
+ const base = createStealthRetryOptions(
528
+ retry.preset ?? HttpRetryPreset.TransportTransient,
529
+ );
530
+ const attempts =
531
+ retry.attempts === undefined || !Number.isFinite(retry.attempts)
532
+ ? base.attempts
533
+ : Math.max(
534
+ 1,
535
+ Math.min(MAX_STEALTH_RETRY_ATTEMPTS, Math.floor(retry.attempts)),
536
+ );
537
+ const normalized: NormalizedStealthRetryOptions = {
538
+ attempts,
539
+ methods:
540
+ retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
541
+ errorCodes: retry.errorCodes ?? base.errorCodes,
542
+ unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
543
+ };
544
+
545
+ if (
546
+ normalized.unsafeMethodPolicy !==
547
+ HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
548
+ ) {
549
+ const unsafeMethods = normalized.methods.filter((method) =>
550
+ UNSAFE_STEALTH_RETRY_METHODS.has(method.toUpperCase()),
551
+ );
552
+ if (unsafeMethods.length > 0) {
553
+ throw new ProviderError(
554
+ `Stealth retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
555
+ { code: "retry_unsafe_method" },
556
+ );
557
+ }
558
+ }
559
+
560
+ return normalized;
561
+ }
562
+
563
+ function isExplicitStealthRetryAllowed(
564
+ method: StealthMethod,
565
+ error: TransportError,
566
+ retryOptions: NormalizedStealthRetryOptions | undefined,
567
+ ): boolean {
568
+ if (!retryOptions || retryOptions.attempts <= 1) return false;
569
+ return (
570
+ retryOptions.methods.includes(method.toUpperCase()) &&
571
+ retryOptions.errorCodes.includes(proxyAttemptErrorCode(error))
572
+ );
573
+ }
574
+
575
+ function isRetryableProxyTransportError(error: unknown): boolean {
576
+ if (error instanceof TransportError) {
577
+ if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
578
+ return false;
579
+ }
580
+ return (
581
+ error.code === PROXY_CONNECT_FAILURE_CODE ||
582
+ error.code === "transport_network_error" ||
583
+ error.code === "transport_timeout"
584
+ );
585
+ }
586
+
587
+ if (error instanceof SDKError) {
588
+ return false;
589
+ }
590
+
591
+ const message = error instanceof Error ? error.message : String(error);
592
+ return /\bproxy\b|\bnon[\s-]?200\b|\bconnect\b|\btunnel\b/i.test(message);
593
+ }
594
+
595
+ function isProxyConnectFailureResponse(
596
+ response: StealthTransportResponse,
597
+ body: string,
598
+ ): boolean {
599
+ return (
600
+ response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "")
601
+ );
602
+ }
603
+
604
+ function createProxyConnectFailureError(
605
+ body: string,
606
+ cause?: Error,
607
+ ): TransportError {
608
+ const bodyExcerpt = (body ?? "").trim().slice(0, 1_000);
609
+ if (isProxyAuthIpDeniedMessage(bodyExcerpt)) {
610
+ return createProxyAuthIpDeniedError(cause);
611
+ }
612
+ if (isProxyEdgeAuthRejectedMessage(bodyExcerpt)) {
613
+ return createProxyEdgeAuthRejectedError(cause);
614
+ }
615
+ if (isProxyPoolStaleMessage(bodyExcerpt)) {
616
+ return createProxyPoolStaleError(
617
+ bodyExcerpt.includes("512") ? 512 : 509,
618
+ cause,
619
+ );
620
+ }
621
+ return new TransportError(bodyExcerpt || "Proxy CONNECT failed", {
622
+ code: PROXY_CONNECT_FAILURE_CODE,
623
+ status: 0,
624
+ cause,
625
+ });
626
+ }
627
+
628
+ function shouldRunProxyAuthDiagnostic(error: unknown): boolean {
629
+ if (!(error instanceof TransportError)) {
630
+ return false;
631
+ }
632
+ if (error.code !== PROXY_POOL_STALE_CODE || error.status !== 512) {
633
+ return false;
634
+ }
635
+
636
+ return error.cause instanceof Error;
637
+ }
638
+
639
+ type ResolvedAttemptProxy = {
640
+ url?: string;
641
+ poolIndex?: number;
642
+ proxyHash?: string;
643
+ };
644
+
645
+ function proxyPoolIndexFromDiagnostics(
646
+ diagnostics: Record<string, string | number | boolean> | undefined,
647
+ ): number | undefined {
648
+ const value = diagnostics?.poolIndex;
649
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
650
+ return undefined;
651
+ }
652
+ return Math.floor(value);
653
+ }
654
+
655
+ function proxyEndpointHash(proxyUrl: string | undefined): string | undefined {
656
+ if (!proxyUrl) return undefined;
657
+ try {
658
+ const parsed = new URL(proxyUrl);
659
+ return createHash("sha256")
660
+ .update(`${parsed.protocol}//${parsed.host}`)
661
+ .digest("hex")
662
+ .slice(0, 12);
663
+ } catch {
664
+ return createHash("sha256").update(proxyUrl).digest("hex").slice(0, 12);
665
+ }
666
+ }
667
+
668
+ function getProxyTunnelStatus(error: unknown): number | undefined {
669
+ if (isRecord(error)) {
670
+ const status = error.status;
671
+ if (typeof status === "number" && Number.isFinite(status)) {
672
+ return status;
673
+ }
674
+ }
675
+
676
+ const cause = error instanceof Error ? error.cause : undefined;
677
+ if (cause && cause !== error) {
678
+ return getProxyTunnelStatus(cause);
679
+ }
680
+
681
+ return undefined;
682
+ }
683
+
684
+ function isTimeoutError(error: unknown, message: string): boolean {
685
+ if (error instanceof Error) {
686
+ if (error.name === "AbortError" || error.name === "TimeoutError") {
687
+ return true;
688
+ }
689
+ }
690
+
691
+ return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
692
+ }
693
+
694
+ function normalizeStealthTransportError(error: unknown): TransportError {
695
+ if (error instanceof ProxyResolutionError) {
696
+ return new TransportError(error.message, {
697
+ code: error.code,
698
+ status: 0,
699
+ cause: error,
700
+ });
701
+ }
702
+
703
+ if (error instanceof TransportError) {
704
+ return error;
705
+ }
706
+
707
+ if (error instanceof SDKError) {
708
+ throw error;
709
+ }
710
+
711
+ const message =
712
+ error instanceof Error
713
+ ? [error.message, error.cause instanceof Error ? error.cause.message : ""]
714
+ .filter(Boolean)
715
+ .join(" ")
716
+ : String(error);
717
+ if (isTimeoutError(error, message)) {
718
+ return new TransportError("Request timed out", {
719
+ code: "transport_timeout",
720
+ status: 0,
721
+ cause: error instanceof Error ? error : undefined,
722
+ });
723
+ }
724
+
725
+ if (isProxyAuthIpDeniedMessage(message)) {
726
+ return createProxyAuthIpDeniedError(
727
+ error instanceof Error ? error : undefined,
728
+ );
729
+ }
730
+
731
+ if (isProxyEdgeAuthRejectedMessage(message)) {
732
+ return createProxyEdgeAuthRejectedError(
733
+ error instanceof Error ? error : undefined,
734
+ );
735
+ }
736
+
737
+ const proxyTunnelStatus = getProxyTunnelStatus(error);
738
+ if (
739
+ proxyTunnelStatus !== undefined &&
740
+ isProxyPoolStaleStatus(proxyTunnelStatus)
741
+ ) {
742
+ return createProxyPoolStaleError(
743
+ proxyTunnelStatus,
744
+ error instanceof Error ? error : undefined,
745
+ );
746
+ }
747
+
748
+ if (PROXY_CONNECT_FAILURE_BODY_PATTERN.test(message)) {
749
+ return createProxyConnectFailureError(
750
+ message,
751
+ error instanceof Error ? error : undefined,
752
+ );
753
+ }
754
+
755
+ return new TransportError("Network error", {
756
+ code: "transport_network_error",
757
+ status: 0,
758
+ cause: error instanceof Error ? error : undefined,
759
+ });
760
+ }
761
+
762
+ function normalizeMethod(method: HttpMethod | string): StealthMethod {
763
+ switch (method.toUpperCase()) {
764
+ case "HEAD":
765
+ return "HEAD";
766
+ case "GET":
767
+ return "GET";
768
+ case "POST":
769
+ return "POST";
770
+ case "PUT":
771
+ return "PUT";
772
+ case "DELETE":
773
+ return "DELETE";
774
+ case "OPTIONS":
775
+ return "OPTIONS";
776
+ case "TRACE":
777
+ return "TRACE";
778
+ case "PATCH":
779
+ return "PATCH";
780
+ default:
781
+ throw new SDKError(`Unsupported stealth method: ${method}`);
782
+ }
783
+ }
784
+
785
+ function isRedirectStatus(status: number): boolean {
786
+ return [301, 302, 303, 307, 308].includes(status);
787
+ }
788
+
789
+ function nextRedirectMethod(
790
+ status: number,
791
+ method: StealthMethod,
792
+ ): StealthMethod {
793
+ if (status === 303 && method !== "HEAD") return "GET";
794
+ if ((status === 301 || status === 302) && method === "POST") return "GET";
795
+ return method;
796
+ }
797
+
798
+ function locationHeader(headers: Record<string, string>): string | undefined {
799
+ for (const [name, value] of Object.entries(headers)) {
800
+ if (name.toLowerCase() === "location") return value;
801
+ }
802
+ return undefined;
803
+ }
804
+
805
+ function createSessionFetcher(
806
+ baseUrl: string,
807
+ defaultProfile: string,
808
+ clientOptions: StealthClientOptions,
809
+ ): StealthSession {
810
+ const clients = new Map<string, Impit>();
811
+ let closed = false;
812
+ let hasWarnedMissingProxy = false;
813
+ const warn = clientOptions.warn ?? console.warn;
814
+ const cookieJar = new CookieJarImpl([]);
815
+ const impitCookieJar = toImpitCookieJar(cookieJar);
816
+
817
+ function getClient(
818
+ profileName: string,
819
+ proxyUrl: string | undefined,
820
+ ignoreTlsErrors: boolean,
821
+ ): Impit {
822
+ if (closed) {
823
+ throw new TransportError("Stealth session is closed", { status: 0 });
824
+ }
825
+ const browser = resolveImpitBrowser(profileName);
826
+ const cacheKey = JSON.stringify({ browser, proxyUrl, ignoreTlsErrors });
827
+ let client = clients.get(cacheKey);
828
+ if (!client) {
829
+ client = new Impit({
830
+ browser,
831
+ cookieJar: impitCookieJar,
832
+ ...(proxyUrl ? { proxyUrl } : {}),
833
+ ...(ignoreTlsErrors ? { ignoreTlsErrors: true } : {}),
834
+ timeout: 30_000,
835
+ });
836
+ clients.set(cacheKey, client);
837
+ }
838
+ return client;
839
+ }
840
+
841
+ async function resolveRequestProxy(
842
+ options?: StealthFetchOptions,
843
+ proxyAttempt?: number,
844
+ ): Promise<ResolvedAttemptProxy> {
845
+ const rawProxyAttemptOffset = options?.proxyAttemptOffset ?? 0;
846
+ const proxyAttemptOffset = Number.isFinite(rawProxyAttemptOffset)
847
+ ? Math.max(0, Math.floor(rawProxyAttemptOffset))
848
+ : 0;
849
+ const resolvedProxy = await resolveProxyConfigAsync({
850
+ proxy: options?.proxy ?? clientOptions.proxy,
851
+ upstream: clientOptions.upstream,
852
+ apifuseConfig: clientOptions.apifuseConfig,
853
+ affinityKey: clientOptions.affinityKey,
854
+ proxyAttempt:
855
+ proxyAttempt === undefined
856
+ ? proxyAttemptOffset
857
+ : proxyAttemptOffset + proxyAttempt,
858
+ telemetry: clientOptions.telemetry,
859
+ });
860
+
861
+ if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
862
+ hasWarnedMissingProxy = true;
863
+ warn(MISSING_PROXY_WARNING);
864
+ }
865
+
866
+ return {
867
+ url: resolvedProxy.url,
868
+ poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
869
+ proxyHash: proxyEndpointHash(resolvedProxy.url),
870
+ };
871
+ }
872
+
873
+ const session: StealthSession = {
874
+ async fetch(url, options: StealthFetchOptions = {}) {
875
+ const method = normalizeMethod(options.method ?? "GET");
876
+ const hasExplicitRetryPolicy = options.retry !== undefined;
877
+ const stealthRetryOptions = normalizeStealthRetryOptions(options.retry);
878
+ const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
879
+ const usesPolicyAllocator =
880
+ hasPolicyProxy && !options.proxy && !clientOptions.proxy;
881
+ const maxAttempts = usesPolicyAllocator
882
+ ? Math.max(
883
+ 1,
884
+ Math.min(
885
+ MAX_POLICY_PROXY_RETRY_ATTEMPTS,
886
+ clientOptions.proxyPolicy?.session?.poolSize ??
887
+ (typeof clientOptions.upstream?.proxy === "object"
888
+ ? clientOptions.upstream.proxy.session?.poolSize
889
+ : undefined) ??
890
+ DEFAULT_SMARTPROXY_POOL_SIZE,
891
+ ),
892
+ )
893
+ : 1;
894
+ let lastError: unknown;
895
+
896
+ for (
897
+ let refreshAttempt = 0;
898
+ refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES;
899
+ refreshAttempt += 1
900
+ ) {
901
+ let stalePoolError: unknown;
902
+ let stalePoolDiagnosticProxy: string | undefined;
903
+ const attemptedProxies = new Set<string>();
904
+
905
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
906
+ let proxy: string | undefined;
907
+ let attemptProxy: ResolvedAttemptProxy | undefined;
908
+ const attemptStartedAt = Date.now();
909
+ let attemptRecorded = false;
910
+ const recordProxyAttempt = (
911
+ outcome: "ok" | "error",
912
+ errorCode?: string,
913
+ status?: number,
914
+ ) => {
915
+ if (attemptRecorded || !proxy) return;
916
+ attemptRecorded = true;
917
+ clientOptions.telemetry?.recordProxyAttempt?.({
918
+ provider: "smartproxy",
919
+ attempt: attempt + 1,
920
+ ...(attemptProxy?.poolIndex === undefined
921
+ ? {}
922
+ : { poolIndex: attemptProxy.poolIndex }),
923
+ ...(attemptProxy?.proxyHash
924
+ ? { proxyHash: attemptProxy.proxyHash }
925
+ : {}),
926
+ outcome,
927
+ ...(errorCode ? { errorCode } : {}),
928
+ ...(status === undefined ? {} : { status }),
929
+ durationMs: Date.now() - attemptStartedAt,
930
+ });
931
+ };
932
+ try {
933
+ assertNoUnsupportedFingerprintOverrides(options);
934
+ attemptProxy = await resolveRequestProxy(options, attempt);
935
+ proxy = attemptProxy.url;
936
+ if (proxy) {
937
+ if (attemptedProxies.has(proxy)) {
938
+ break;
939
+ }
940
+ attemptedProxies.add(proxy);
941
+ }
942
+ const ignoreTlsErrors = Boolean(
943
+ options.stealth?.insecureSkipVerify ??
944
+ (!hasPolicyProxy &&
945
+ proxy &&
946
+ clientOptions.proxyStealth?.insecureSkipVerify),
947
+ );
948
+ const profileName = options.profile ?? defaultProfile;
949
+ const requestUrl = appendQueryParams(
950
+ resolveUrl(baseUrl, url),
951
+ options.params,
952
+ );
953
+ const headers = { ...(options.headers ?? {}) };
954
+ if (!hasHeader(headers, "Cookie")) {
955
+ const cookieHeader = cookieJar.toString();
956
+ if (cookieHeader) headers.Cookie = cookieHeader;
957
+ }
958
+ const requestInit: StealthRequestInit = {
959
+ headers: normalizeHeaders(headers),
960
+ method,
961
+ ...(options.redirect ? { redirect: options.redirect } : {}),
962
+ ...(options.timeout ? { timeout: options.timeout } : {}),
963
+ };
964
+ if (options.body !== undefined) {
965
+ requestInit.body = normalizeBody(options.body);
966
+ }
967
+ const response = await getClient(
968
+ profileName,
969
+ proxy,
970
+ ignoreTlsErrors,
971
+ ).fetch(requestUrl, requestInit);
972
+ const normalized = await normalizeResponse(response, requestUrl);
973
+ cookieJar.setFromCookieStrings(
974
+ setCookieHeadersFromResponse(response.headers),
975
+ );
976
+
977
+ if (
978
+ proxy &&
979
+ isProxyConnectFailureResponse(response, normalized.body)
980
+ ) {
981
+ throw createProxyConnectFailureError(normalized.body);
982
+ }
983
+
984
+ if (response.status >= 400) {
985
+ if (
986
+ proxy &&
987
+ usesPolicyAllocator &&
988
+ isProxyEdgeTlsRejectedResponse(
989
+ response.status,
990
+ [
991
+ JSON.stringify(responseHeadersToRecord(response.headers)),
992
+ normalized.body,
993
+ ].join("\n"),
994
+ )
995
+ ) {
996
+ throw createProxyEdgeTlsRejectedError(response.status);
997
+ }
998
+ if (proxy && isProxyAuthIpDeniedMessage(normalized.body)) {
999
+ throw createProxyAuthIpDeniedError();
1000
+ }
1001
+ if (proxy && isProxyEdgeAuthRejectedMessage(normalized.body)) {
1002
+ throw createProxyEdgeAuthRejectedError();
1003
+ }
1004
+ if (
1005
+ proxy &&
1006
+ isProxyPoolStaleStatus(response.status) &&
1007
+ isProxyPoolStaleMessage(normalized.body)
1008
+ ) {
1009
+ throw createProxyPoolStaleError(response.status);
1010
+ }
1011
+ }
1012
+
1013
+ if (response.status >= 400 && options.throwOnHttpError !== false) {
1014
+ throw new TransportError(
1015
+ `Upstream request failed with status ${response.status}`,
1016
+ {
1017
+ code: "upstream_http_error",
1018
+ status: response.status,
1019
+ },
1020
+ );
1021
+ }
1022
+
1023
+ recordProxyAttempt("ok", undefined, response.status);
1024
+ return normalized;
1025
+ } catch (error) {
1026
+ const normalizedError = normalizeStealthTransportError(error);
1027
+ recordProxyAttempt(
1028
+ "error",
1029
+ proxyAttemptErrorCode(normalizedError),
1030
+ proxyAttemptStatus(normalizedError),
1031
+ );
1032
+ lastError = normalizedError;
1033
+ if (
1034
+ proxy &&
1035
+ usesPolicyAllocator &&
1036
+ isProxyPoolRefreshableError(normalizedError)
1037
+ ) {
1038
+ stalePoolError = normalizedError;
1039
+ if (shouldRunProxyAuthDiagnostic(normalizedError)) {
1040
+ stalePoolDiagnosticProxy = proxy;
1041
+ }
1042
+ if (attempt + 1 < maxAttempts) {
1043
+ continue;
1044
+ }
1045
+ break;
1046
+ }
1047
+ if (
1048
+ proxy &&
1049
+ attempt + 1 <
1050
+ (stealthRetryOptions
1051
+ ? Math.min(maxAttempts, stealthRetryOptions.attempts)
1052
+ : maxAttempts) &&
1053
+ (!hasExplicitRetryPolicy
1054
+ ? isRetrySafeStealthMethod(method)
1055
+ : isExplicitStealthRetryAllowed(
1056
+ method,
1057
+ normalizedError,
1058
+ stealthRetryOptions,
1059
+ )) &&
1060
+ isRetryableProxyTransportError(normalizedError)
1061
+ ) {
1062
+ continue;
1063
+ }
1064
+ throw normalizedError;
1065
+ }
1066
+ }
1067
+
1068
+ if (
1069
+ usesPolicyAllocator &&
1070
+ stalePoolError &&
1071
+ refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
1072
+ ) {
1073
+ await invalidateProxyResolutionCacheAsync({
1074
+ proxyPolicy: clientOptions.proxyPolicy,
1075
+ upstream: clientOptions.upstream,
1076
+ affinityKey: clientOptions.affinityKey,
1077
+ });
1078
+ continue;
1079
+ }
1080
+
1081
+ const proxyAuthDiagnostic =
1082
+ stalePoolError && stalePoolDiagnosticProxy
1083
+ ? await classifyProxyAuthDiagnostic(
1084
+ options.profile ?? defaultProfile,
1085
+ stalePoolDiagnosticProxy,
1086
+ )
1087
+ : undefined;
1088
+ if (proxyAuthDiagnostic === "source_ip_denied") {
1089
+ throw createProxyAuthIpDeniedError(
1090
+ stalePoolError instanceof Error ? stalePoolError : undefined,
1091
+ );
1092
+ }
1093
+ if (proxyAuthDiagnostic === "edge_auth_rejected") {
1094
+ throw createProxyEdgeAuthRejectedError(
1095
+ stalePoolError instanceof Error ? stalePoolError : undefined,
1096
+ );
1097
+ }
1098
+
1099
+ if (stalePoolError) {
1100
+ if (
1101
+ stalePoolError instanceof TransportError &&
1102
+ stalePoolError.code === PROXY_EDGE_AUTH_REJECTED_CODE
1103
+ ) {
1104
+ throw stalePoolError;
1105
+ }
1106
+ throw createProxyPoolExhaustedError(
1107
+ stalePoolError instanceof Error ? stalePoolError : undefined,
1108
+ );
1109
+ }
1110
+ break;
1111
+ }
1112
+
1113
+ throw normalizeStealthTransportError(lastError);
1114
+ },
1115
+ cookies: cookieJar,
1116
+ redirects: {
1117
+ async run(options) {
1118
+ const maxHops =
1119
+ options.maxHops === undefined || !Number.isFinite(options.maxHops)
1120
+ ? 10
1121
+ : Math.max(0, Math.floor(options.maxHops));
1122
+ const hops: StealthRedirectHop[] = [];
1123
+ let currentUrl = resolveUrl(baseUrl, options.url);
1124
+ let method = normalizeMethod(options.method ?? "GET");
1125
+ let body = options.body;
1126
+ let response: StealthResponse | undefined;
1127
+ const visitedRequests = new Set<string>();
1128
+
1129
+ const {
1130
+ url: _url,
1131
+ maxHops: _maxHops,
1132
+ stopWhen,
1133
+ params,
1134
+ ...fetchOptions
1135
+ } = options;
1136
+
1137
+ for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
1138
+ visitedRequests.add(`${method} ${currentUrl}`);
1139
+ response = await session.fetch(currentUrl, {
1140
+ ...fetchOptions,
1141
+ body,
1142
+ method,
1143
+ ...(hopIndex === 0 && params ? { params } : {}),
1144
+ redirect: "manual",
1145
+ throwOnHttpError: false,
1146
+ });
1147
+
1148
+ if (!isRedirectStatus(response.status)) {
1149
+ return {
1150
+ final: response,
1151
+ hops,
1152
+ reason: "completed",
1153
+ cookies: cookieJar.snapshot(),
1154
+ };
1155
+ }
1156
+
1157
+ const location = locationHeader(response.headers);
1158
+ const nextUrl = location
1159
+ ? new URL(location, response.url ?? currentUrl).toString()
1160
+ : undefined;
1161
+ const hop: StealthRedirectHop = {
1162
+ url: response.url ?? currentUrl,
1163
+ status: response.status,
1164
+ method,
1165
+ ...(location ? { location } : {}),
1166
+ ...(nextUrl ? { nextUrl } : {}),
1167
+ };
1168
+ hops.push(hop);
1169
+
1170
+ if (stopWhen && (await stopWhen(hop))) {
1171
+ return {
1172
+ final: response,
1173
+ hops,
1174
+ reason: "stopped",
1175
+ cookies: cookieJar.snapshot(),
1176
+ };
1177
+ }
1178
+
1179
+ if (!nextUrl) {
1180
+ return {
1181
+ final: response,
1182
+ hops,
1183
+ reason: "missing_location",
1184
+ cookies: cookieJar.snapshot(),
1185
+ };
1186
+ }
1187
+
1188
+ if (hops.length > maxHops) {
1189
+ return {
1190
+ final: response,
1191
+ hops,
1192
+ reason: "max_hops",
1193
+ cookies: cookieJar.snapshot(),
1194
+ };
1195
+ }
1196
+
1197
+ const nextMethod = nextRedirectMethod(response.status, method);
1198
+ if (nextMethod !== method) {
1199
+ body = undefined;
1200
+ }
1201
+ if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
1202
+ return {
1203
+ final: response,
1204
+ hops,
1205
+ reason: "loop",
1206
+ cookies: cookieJar.snapshot(),
1207
+ };
1208
+ }
1209
+ method = nextMethod;
1210
+ currentUrl = nextUrl;
1211
+ }
1212
+
1213
+ if (!response) {
1214
+ response = await session.fetch(currentUrl, {
1215
+ ...fetchOptions,
1216
+ body,
1217
+ method,
1218
+ ...(params ? { params } : {}),
1219
+ redirect: "manual",
1220
+ throwOnHttpError: false,
1221
+ });
1222
+ }
1223
+ return {
1224
+ final: response,
1225
+ hops,
1226
+ reason: "max_hops",
1227
+ cookies: cookieJar.snapshot(),
1228
+ };
1229
+ },
1230
+ },
1231
+ close() {
1232
+ closed = true;
1233
+ clients.clear();
1234
+ },
1235
+ };
1236
+ return session;
1237
+
1238
+ async function classifyProxyAuthDiagnostic(
1239
+ profileName: string,
1240
+ proxy: string,
1241
+ ): Promise<"source_ip_denied" | "edge_auth_rejected" | undefined> {
1242
+ try {
1243
+ const response = await getClient(profileName, proxy, false).fetch(
1244
+ PROXY_AUTH_DIAGNOSTIC_URL,
1245
+ {
1246
+ method: "GET",
1247
+ timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
1248
+ },
1249
+ );
1250
+ const normalized = await normalizeResponse(response);
1251
+ return classifyProxyAuthDiagnosticMessage(normalized.body);
1252
+ } catch (error) {
1253
+ const message =
1254
+ error instanceof Error
1255
+ ? [
1256
+ error.message,
1257
+ error.cause instanceof Error ? error.cause.message : "",
1258
+ ]
1259
+ .filter(Boolean)
1260
+ .join(" ")
1261
+ : String(error);
1262
+ return classifyProxyAuthDiagnosticMessage(message);
1263
+ }
1264
+ }
1265
+ }
1266
+
1267
+ function classifyProxyAuthDiagnosticMessage(
1268
+ message: string,
1269
+ ): "source_ip_denied" | "edge_auth_rejected" | undefined {
1270
+ if (isProxyAuthIpDeniedMessage(message)) {
1271
+ return "source_ip_denied";
1272
+ }
1273
+ if (isProxyEdgeAuthRejectedMessage(message)) {
1274
+ return "edge_auth_rejected";
1275
+ }
1276
+ return undefined;
1277
+ }
1278
+
1279
+ function proxyAttemptErrorCode(error: TransportError): string {
1280
+ return error.code ?? error.name ?? "transport_error";
1281
+ }
1282
+
1283
+ function proxyAttemptStatus(error: TransportError): number | undefined {
1284
+ return error.status ?? error.upstreamStatus;
1285
+ }
1286
+
1287
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
1288
+ const needle = name.toLowerCase();
1289
+ return Object.keys(headers).some((key) => key.toLowerCase() === needle);
1290
+ }
1291
+
1292
+ export function createStealthClient(
1293
+ baseUrl: string,
1294
+ defaultProfileOrOptions: string | StealthClientOptions = DEFAULT_PROFILE,
1295
+ clientOptions: StealthClientOptions = {},
1296
+ ): StealthClient {
1297
+ const defaultProfile =
1298
+ typeof defaultProfileOrOptions === "string"
1299
+ ? defaultProfileOrOptions
1300
+ : DEFAULT_PROFILE;
1301
+ const resolvedClientOptions =
1302
+ typeof defaultProfileOrOptions === "string"
1303
+ ? clientOptions
1304
+ : defaultProfileOrOptions;
1305
+ let sharedSession: StealthSession | null = null;
1306
+
1307
+ function getSharedSession(): StealthSession {
1308
+ if (!sharedSession) {
1309
+ sharedSession = createSessionFetcher(
1310
+ baseUrl,
1311
+ defaultProfile,
1312
+ resolvedClientOptions,
1313
+ );
1314
+ }
1315
+
1316
+ return sharedSession;
1317
+ }
1318
+
1319
+ return {
1320
+ fetch(url: string, options?: StealthFetchOptions) {
1321
+ return getSharedSession().fetch(url, options);
1322
+ },
1323
+ createSession(opts?: { profile?: string }) {
1324
+ const sessionProfile = opts?.profile ?? defaultProfile;
1325
+ return createSessionFetcher(
1326
+ baseUrl,
1327
+ sessionProfile,
1328
+ resolvedClientOptions,
1329
+ );
1330
+ },
1331
+ close() {
1332
+ sharedSession?.close();
1333
+ sharedSession = null;
1334
+ },
1335
+ };
1336
+ }