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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +165 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +88 -4
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-pack-check.ts +22 -2
  8. package/bin/apifuse-pack-smoke.ts +57 -2
  9. package/bin/apifuse-pack-types.ts +356 -38
  10. package/bin/apifuse-perf.ts +14 -13
  11. package/bin/apifuse-record.ts +691 -68
  12. package/bin/apifuse-submit-check.ts +518 -37
  13. package/bin/apifuse-sync-assets.ts +117 -0
  14. package/bin/submit-check-delimited-text.ts +50 -0
  15. package/dist/auth-turn/index.d.ts +3 -3
  16. package/dist/auth-turn/index.js +1 -1
  17. package/dist/auth.d.ts +14 -0
  18. package/dist/auth.js +67 -0
  19. package/dist/ceremonies/index.d.ts +16 -0
  20. package/dist/ceremonies/index.js +141 -36
  21. package/dist/cli/commands.d.ts +1 -1
  22. package/dist/cli/commands.js +8 -0
  23. package/dist/cli/create.d.ts +3 -0
  24. package/dist/cli/create.js +34 -35
  25. package/dist/cli/prompt-assets.d.ts +80 -0
  26. package/dist/cli/prompt-assets.js +743 -0
  27. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  29. package/dist/cli/templates/provider/README.md.tpl +4 -4
  30. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  31. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  32. package/dist/config/loader.d.ts +177 -16
  33. package/dist/config/loader.js +424 -127
  34. package/dist/contract-serialization.js +4 -8
  35. package/dist/contract-types.d.ts +1 -0
  36. package/dist/contract.js +2 -0
  37. package/dist/declaration-validation.d.ts +32 -0
  38. package/dist/declaration-validation.js +207 -0
  39. package/dist/define.d.ts +51 -25
  40. package/dist/define.js +752 -38
  41. package/dist/error-resolution.d.ts +4 -0
  42. package/dist/error-resolution.js +122 -0
  43. package/dist/errors.d.ts +18 -0
  44. package/dist/errors.js +40 -0
  45. package/dist/fixture-sanitization.d.ts +28 -0
  46. package/dist/fixture-sanitization.js +217 -0
  47. package/dist/health-scenario.d.ts +1842 -0
  48. package/dist/health-scenario.js +624 -0
  49. package/dist/index.d.ts +19 -9
  50. package/dist/index.js +10 -6
  51. package/dist/lint.d.ts +6 -1
  52. package/dist/lint.js +362 -3
  53. package/dist/native-address.d.ts +43 -0
  54. package/dist/native-address.js +281 -0
  55. package/dist/native-egress-policy.d.ts +31 -0
  56. package/dist/native-egress-policy.js +288 -0
  57. package/dist/observability.d.ts +5 -2
  58. package/dist/observability.js +48 -1
  59. package/dist/provider.d.ts +8 -2
  60. package/dist/provider.js +3 -1
  61. package/dist/runtime/auth-flow.d.ts +5 -1
  62. package/dist/runtime/auth-flow.js +6 -0
  63. package/dist/runtime/browser.d.ts +1 -0
  64. package/dist/runtime/browser.js +492 -49
  65. package/dist/runtime/cache.d.ts +1 -0
  66. package/dist/runtime/cache.js +169 -15
  67. package/dist/runtime/choice-wordlist.d.ts +9 -0
  68. package/dist/runtime/choice-wordlist.js +138 -0
  69. package/dist/runtime/choice.d.ts +13 -1
  70. package/dist/runtime/choice.js +490 -102
  71. package/dist/runtime/executor.d.ts +2 -2
  72. package/dist/runtime/executor.js +26 -2
  73. package/dist/runtime/http.d.ts +1 -0
  74. package/dist/runtime/http.js +515 -53
  75. package/dist/runtime/instrumentation.d.ts +2 -2
  76. package/dist/runtime/instrumentation.js +366 -8
  77. package/dist/runtime/native-network-errors.d.ts +33 -0
  78. package/dist/runtime/native-network-errors.js +69 -0
  79. package/dist/runtime/native-network.d.ts +96 -0
  80. package/dist/runtime/native-network.js +1232 -0
  81. package/dist/runtime/ocr.d.ts +29 -0
  82. package/dist/runtime/ocr.js +440 -0
  83. package/dist/runtime/proxy-errors.js +6 -2
  84. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  85. package/dist/runtime/proxy-nodemaven.js +146 -0
  86. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  87. package/dist/runtime/proxy-telemetry.js +154 -47
  88. package/dist/runtime/redirects.d.ts +29 -0
  89. package/dist/runtime/redirects.js +36 -0
  90. package/dist/runtime/redis.d.ts +1 -1
  91. package/dist/runtime/redis.js +4 -2
  92. package/dist/runtime/request-options.d.ts +68 -1
  93. package/dist/runtime/request-options.js +548 -0
  94. package/dist/runtime/resolver-config.d.ts +6 -0
  95. package/dist/runtime/resolver-config.js +6 -0
  96. package/dist/runtime/resolver-public.d.ts +1 -0
  97. package/dist/runtime/resolver-public.js +1 -0
  98. package/dist/runtime/resolver-shared.d.ts +3 -0
  99. package/dist/runtime/resolver-shared.js +12 -0
  100. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  101. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  102. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  103. package/dist/runtime/resolver-vendors/browser.js +377 -0
  104. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  105. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  106. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  107. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  108. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  109. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  110. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  111. package/dist/runtime/resolver-vendors/types.js +96 -0
  112. package/dist/runtime/resolver.d.ts +60 -0
  113. package/dist/runtime/resolver.js +737 -0
  114. package/dist/runtime/secrets.d.ts +27 -0
  115. package/dist/runtime/secrets.js +51 -0
  116. package/dist/runtime/state.d.ts +3 -0
  117. package/dist/runtime/state.js +277 -71
  118. package/dist/runtime/stealth-cookies.d.ts +20 -0
  119. package/dist/runtime/stealth-cookies.js +111 -0
  120. package/dist/runtime/stealth.d.ts +28 -3
  121. package/dist/runtime/stealth.js +519 -255
  122. package/dist/runtime/stt.js +1 -12
  123. package/dist/runtime/timeout.d.ts +5 -0
  124. package/dist/runtime/timeout.js +12 -0
  125. package/dist/runtime/trace-config.d.ts +12 -0
  126. package/dist/runtime/trace-config.js +61 -0
  127. package/dist/serve.d.ts +1 -1
  128. package/dist/serve.js +1 -1
  129. package/dist/server/index.d.ts +5 -3
  130. package/dist/server/index.js +3 -3
  131. package/dist/server/self-test-input-tokens.d.ts +2 -1
  132. package/dist/server/self-test-input-tokens.js +18 -14
  133. package/dist/server/self-test.d.ts +114 -0
  134. package/dist/server/self-test.js +784 -148
  135. package/dist/server/serve-implementation.d.ts +213 -0
  136. package/dist/server/serve-implementation.js +2173 -0
  137. package/dist/server/serve.d.ts +1 -70
  138. package/dist/server/serve.js +1 -1130
  139. package/dist/server/trace-output.d.ts +4 -0
  140. package/dist/server/trace-output.js +20 -0
  141. package/dist/server/types.d.ts +30 -5
  142. package/dist/server/types.js +13 -1
  143. package/dist/stateful/errors.d.ts +19 -0
  144. package/dist/stateful/errors.js +24 -0
  145. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  146. package/dist/stateful/http-provider-event-emitter.js +237 -0
  147. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  148. package/dist/stateful/http-session-owner-registry.js +210 -0
  149. package/dist/stateful/index.d.ts +18 -0
  150. package/dist/stateful/index.js +18 -0
  151. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  152. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  153. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  154. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  155. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  156. package/dist/stateful/provider-event-pipeline.js +1 -0
  157. package/dist/stateful/provider-events.d.ts +101 -0
  158. package/dist/stateful/provider-events.js +289 -0
  159. package/dist/stateful/session-key.d.ts +15 -0
  160. package/dist/stateful/session-key.js +86 -0
  161. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  162. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  163. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  164. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  165. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  166. package/dist/stateful/stateful-provider-adapter.js +287 -0
  167. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  168. package/dist/stateful/stateful-provider-observability.js +161 -0
  169. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  170. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  171. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  172. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  173. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  174. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  175. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  176. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  177. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  178. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  179. package/dist/stateful-signing.d.ts +18 -0
  180. package/dist/stateful-signing.js +27 -0
  181. package/dist/stealth/profiles.js +16 -7
  182. package/dist/stream-evidence.d.ts +74 -0
  183. package/dist/stream-evidence.js +785 -0
  184. package/dist/stream.js +7 -1
  185. package/dist/testing/index.d.ts +2 -1
  186. package/dist/testing/index.js +2 -1
  187. package/dist/testing/run.d.ts +32 -2
  188. package/dist/testing/run.js +489 -21
  189. package/dist/trace-sanitization.d.ts +5 -0
  190. package/dist/trace-sanitization.js +45 -0
  191. package/dist/types.d.ts +545 -23
  192. package/dist/types.js +1 -0
  193. package/package.json +44 -5
  194. package/src/auth-turn/index.ts +1 -1
  195. package/src/auth.ts +118 -0
  196. package/src/ceremonies/index.ts +189 -46
  197. package/src/cli/commands.ts +10 -0
  198. package/src/cli/create.ts +42 -35
  199. package/src/cli/prompt-assets.ts +865 -0
  200. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  201. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  202. package/src/cli/templates/provider/README.md.tpl +4 -4
  203. package/src/cli/templates/provider/index.ts.tpl +6 -3
  204. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  205. package/src/config/loader.ts +665 -163
  206. package/src/contract-serialization.ts +5 -7
  207. package/src/contract-types.ts +1 -0
  208. package/src/contract.ts +2 -0
  209. package/src/declaration-validation.ts +266 -0
  210. package/src/define.ts +970 -87
  211. package/src/error-resolution.ts +127 -0
  212. package/src/errors.ts +52 -0
  213. package/src/fixture-sanitization.ts +248 -0
  214. package/src/health-scenario.ts +875 -0
  215. package/src/index.ts +204 -8
  216. package/src/lint.ts +408 -4
  217. package/src/native-address.ts +340 -0
  218. package/src/native-egress-policy.ts +358 -0
  219. package/src/observability.ts +51 -1
  220. package/src/provider.ts +133 -0
  221. package/src/runtime/auth-flow.ts +12 -0
  222. package/src/runtime/browser.ts +661 -63
  223. package/src/runtime/cache.ts +189 -14
  224. package/src/runtime/choice-wordlist.ts +145 -0
  225. package/src/runtime/choice.ts +631 -120
  226. package/src/runtime/executor.ts +40 -7
  227. package/src/runtime/http.ts +641 -61
  228. package/src/runtime/instrumentation.ts +520 -15
  229. package/src/runtime/native-network-errors.ts +99 -0
  230. package/src/runtime/native-network.ts +1605 -0
  231. package/src/runtime/ocr.ts +523 -0
  232. package/src/runtime/proxy-errors.ts +12 -4
  233. package/src/runtime/proxy-nodemaven.ts +221 -0
  234. package/src/runtime/proxy-telemetry.ts +244 -75
  235. package/src/runtime/redirects.ts +66 -0
  236. package/src/runtime/redis.ts +7 -2
  237. package/src/runtime/request-options.ts +680 -1
  238. package/src/runtime/resolver-config.ts +6 -0
  239. package/src/runtime/resolver-public.ts +20 -0
  240. package/src/runtime/resolver-shared.ts +17 -0
  241. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  242. package/src/runtime/resolver-vendors/browser.ts +533 -0
  243. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  244. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  245. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  246. package/src/runtime/resolver-vendors/types.ts +212 -0
  247. package/src/runtime/resolver.ts +1103 -0
  248. package/src/runtime/secrets.ts +64 -0
  249. package/src/runtime/state.ts +394 -77
  250. package/src/runtime/stealth-cookies.ts +132 -0
  251. package/src/runtime/stealth.ts +675 -289
  252. package/src/runtime/stt.ts +1 -19
  253. package/src/runtime/timeout.ts +18 -0
  254. package/src/runtime/trace-config.ts +77 -0
  255. package/src/serve.ts +6 -1
  256. package/src/server/index.ts +37 -2
  257. package/src/server/self-test-input-tokens.ts +29 -14
  258. package/src/server/self-test.ts +1025 -175
  259. package/src/server/serve-implementation.ts +3250 -0
  260. package/src/server/serve.ts +1 -1626
  261. package/src/server/trace-output.ts +32 -0
  262. package/src/server/types.ts +13 -1
  263. package/src/stateful/README.md +146 -0
  264. package/src/stateful/errors.ts +35 -0
  265. package/src/stateful/http-provider-event-emitter.ts +314 -0
  266. package/src/stateful/http-session-owner-registry.ts +306 -0
  267. package/src/stateful/index.ts +18 -0
  268. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  269. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  270. package/src/stateful/provider-event-pipeline.ts +61 -0
  271. package/src/stateful/provider-events.ts +462 -0
  272. package/src/stateful/session-key.ts +111 -0
  273. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  274. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  275. package/src/stateful/stateful-provider-adapter.ts +562 -0
  276. package/src/stateful/stateful-provider-observability.ts +261 -0
  277. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  278. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  279. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  280. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  281. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  282. package/src/stateful-signing.ts +46 -0
  283. package/src/stealth/profiles.ts +17 -7
  284. package/src/stream-evidence.ts +988 -0
  285. package/src/stream.ts +8 -1
  286. package/src/testing/index.ts +10 -1
  287. package/src/testing/run.ts +658 -15
  288. package/src/trace-sanitization.ts +63 -0
  289. package/src/types.ts +660 -38
  290. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  291. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  292. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  293. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  294. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  295. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  296. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  297. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  298. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  299. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  300. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  301. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  302. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  303. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,11 +1,32 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import path from "node:path";
4
5
 
5
- import { Redis } from "ioredis";
6
-
7
- import type { ProviderProxyPolicy, TraceConfig } from "../types.js";
8
-
6
+ import type { Redis } from "ioredis";
7
+
8
+ import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
9
+ import {
10
+ NODEMAVEN_DEFAULT_PROTOCOL,
11
+ NODEMAVEN_FILTER_ENV,
12
+ NODEMAVEN_MAX_POOL_SIZE,
13
+ NODEMAVEN_PASSWORD_ENV,
14
+ NODEMAVEN_USERNAME_ENV,
15
+ type ProxyProtocol,
16
+ hasNodemavenCredentials,
17
+ nodemavenPoolSize,
18
+ synthesizeNodemavenProxy,
19
+ } from "../runtime/proxy-nodemaven.js";
20
+
21
+ export type { ProxyProtocol } from "../runtime/proxy-nodemaven.js";
22
+
23
+ /** Proxy vendors with SDK-managed resolution. */
24
+ export type ProxyVendorName = "smartproxy" | "nodemaven";
25
+
26
+ // "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
27
+ // extraction API (app_key → raw ip:port pool). It is NOT the company formerly
28
+ // named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
29
+ // modelled separately as the `decodo` gateway vendor. Do not conflate them.
9
30
  export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
10
31
  export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
11
32
  export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
@@ -17,15 +38,6 @@ export const PROVIDER_CACHE_REDIS_URL_ENV = "APIFUSE__PROVIDER__CACHE_REDIS_URL"
17
38
  export const PROVIDER_STATE_REDIS_URL_ENV = "APIFUSE__PROVIDER__STATE_REDIS_URL";
18
39
  export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
19
40
 
20
- export type ProxyOptions = {
21
- url: string;
22
- };
23
-
24
- export type ProxyConfig = Partial<ProxyOptions> & {
25
- provider?: string;
26
- apiKey?: string;
27
- };
28
-
29
41
  export type BrowserConfig = {
30
42
  executablePath?: string;
31
43
  headless?: boolean;
@@ -37,7 +49,6 @@ export type SessionConfig = {
37
49
  };
38
50
 
39
51
  export type ApiFuseConfig = {
40
- proxy?: ProxyConfig;
41
52
  browser?: BrowserConfig;
42
53
  session?: SessionConfig;
43
54
  trace?: TraceConfig;
@@ -47,11 +58,30 @@ export type ApiFuseConfig = {
47
58
  export type ProxyResolutionOptions = {
48
59
  proxy?: string;
49
60
  upstream?: { proxy?: boolean | ProviderProxyPolicy };
50
- apifuseConfig?: Pick<ApiFuseConfig, "proxy">;
51
61
  proxyPolicy?: ProviderProxyPolicy;
52
62
  affinityKey?: string;
53
63
  /** Zero-based proxy-pool attempt index used by SDK transports for failover. */
54
64
  proxyAttempt?: number;
65
+ /**
66
+ * Tunnelling protocols the calling transport can use. When a resolved
67
+ * protocol is not in this set the resolver fails with
68
+ * `PROXY_PROTOCOL_UNSUPPORTED` instead of silently downgrading. Unset means
69
+ * permissive (both protocols allowed).
70
+ */
71
+ transportProtocols?: readonly ProxyProtocol[];
72
+ /**
73
+ * Explicit protocol override. Internal — for the verification harness and
74
+ * tests, or an advanced caller. Normal callers omit it and each vendor uses
75
+ * its own benchmarked default protocol (see VENDOR_DEFAULT_PROTOCOL). Not an
76
+ * env var and not a provider-policy field.
77
+ */
78
+ protocol?: ProxyProtocol;
79
+ /**
80
+ * Gateway pool "refresh" generation. Bumped by transports on pool refresh to
81
+ * derive a fresh gateway session set (ignored by allocation-style vendors,
82
+ * whose refresh is driven by cache invalidation).
83
+ */
84
+ proxyRefreshEpoch?: number;
55
85
  telemetry?: ProxyTelemetrySink;
56
86
  };
57
87
 
@@ -73,8 +103,14 @@ export type SmartproxyAllocatorBodyClass =
73
103
  | "text_without_proxies"
74
104
  | "usable_proxy_endpoints";
75
105
 
106
+ export type ProxyUserAgentSource = "declared" | "defaulted";
107
+
76
108
  export type ProxyResolutionTelemetryEvent = {
77
- provider: "smartproxy";
109
+ provider: ProxyVendorName;
110
+ /** Defaults to `"ok"` when omitted. */
111
+ outcome?: "ok" | "error";
112
+ userAgentSource?: ProxyUserAgentSource;
113
+ protocol?: ProxyProtocol;
78
114
  cacheStatus: ProxyCacheStatus;
79
115
  cacheHit: boolean;
80
116
  resolutionMs: number;
@@ -92,7 +128,7 @@ export type ProxyResolutionTelemetryEvent = {
92
128
  };
93
129
 
94
130
  export type ProxyAttemptTelemetryEvent = {
95
- provider: "smartproxy";
131
+ provider: ProxyVendorName;
96
132
  attempt: number;
97
133
  poolIndex?: number;
98
134
  proxyHash?: string;
@@ -102,31 +138,69 @@ export type ProxyAttemptTelemetryEvent = {
102
138
  durationMs?: number;
103
139
  };
104
140
 
141
+ export type ProxyVendorFailoverTelemetryEvent = {
142
+ /** Vendor that failed or was skipped. */
143
+ vendor: ProxyVendorName;
144
+ /** Vendor tried next, or undefined when the chain is exhausted. */
145
+ nextVendor?: ProxyVendorName;
146
+ phase: "resolution" | "transport";
147
+ reason: "no_credentials" | "allocation_failed" | "pool_exhausted" | "protocol_unsupported";
148
+ attempt?: number;
149
+ };
150
+
105
151
  export type ProxyTelemetrySink = {
106
152
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
107
153
  recordProxyAttempt?(event: ProxyAttemptTelemetryEvent): void;
154
+ recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
108
155
  };
109
156
 
157
+ export type ProxyResolutionSource =
158
+ | "explicit"
159
+ | "env"
160
+ | "config"
161
+ | "smartproxy-allocator"
162
+ | "nodemaven-gateway";
163
+
110
164
  export type ResolvedProxyConfig = {
111
165
  shouldWarn: boolean;
112
166
  url?: string;
113
- source?: "explicit" | "env" | "config" | "smartproxy-allocator";
167
+ /** SDK-native vendor that supplied the URL, when applicable. */
168
+ vendor?: ProxyVendorName;
169
+ source?: ProxyResolutionSource;
170
+ protocol?: ProxyProtocol;
114
171
  diagnostics?: Record<string, string | number | boolean>;
115
172
  };
116
173
 
174
+ export type ProxyResolutionErrorCode =
175
+ | "PROXY_REQUIRED"
176
+ | "PROXY_ALLOCATION_FAILED"
177
+ | "PROXY_PROTOCOL_UNSUPPORTED";
178
+
117
179
  export class ProxyResolutionError extends Error {
118
- readonly code: "PROXY_REQUIRED" | "PROXY_ALLOCATION_FAILED";
180
+ readonly code: ProxyResolutionErrorCode;
119
181
  readonly telemetry?: ProxyResolutionTelemetryEvent;
182
+ readonly vendor?: ProxyVendorName;
183
+ readonly vendorChain?: ProxyVendorName[];
184
+ readonly protocol?: ProxyProtocol;
120
185
 
121
186
  constructor(
122
- code: "PROXY_REQUIRED" | "PROXY_ALLOCATION_FAILED",
187
+ code: ProxyResolutionErrorCode,
123
188
  message: string,
124
- options?: { cause?: unknown; telemetry?: ProxyResolutionTelemetryEvent },
189
+ options?: {
190
+ cause?: unknown;
191
+ telemetry?: ProxyResolutionTelemetryEvent;
192
+ vendor?: ProxyVendorName;
193
+ vendorChain?: ProxyVendorName[];
194
+ protocol?: ProxyProtocol;
195
+ },
125
196
  ) {
126
197
  super(message, options);
127
198
  this.name = "ProxyResolutionError";
128
199
  this.code = code;
129
200
  this.telemetry = options?.telemetry;
201
+ this.vendor = options?.vendor;
202
+ this.vendorChain = options?.vendorChain;
203
+ this.protocol = options?.protocol;
130
204
  }
131
205
  }
132
206
 
@@ -149,6 +223,7 @@ type ProxyRedisClient = Pick<
149
223
  >;
150
224
 
151
225
  const proxyCache = new Map<string, CachedProxyPool>();
226
+ const require = createRequire(import.meta.url);
152
227
  const proxyInflight = new Map<string, Promise<SmartproxyAllocationResult>>();
153
228
  const invalidatedProxyKeys = new Map<string, number>();
154
229
  const redisClients = new Map<string, ProxyRedisClient>();
@@ -208,7 +283,8 @@ function getProxyRedis(): ProxyRedisClient | undefined {
208
283
  const existing = redisClients.get(redisUrl);
209
284
  if (existing) return existing;
210
285
 
211
- const redis = new Redis(redisUrl, {
286
+ const { Redis: RedisClient } = require("ioredis") as typeof import("ioredis");
287
+ const redis = new RedisClient(redisUrl, {
212
288
  connectTimeout: REDIS_TIMEOUT_MS,
213
289
  enableOfflineQueue: false,
214
290
  lazyConnect: true,
@@ -296,6 +372,7 @@ function telemetryForFailure(
296
372
  ): ProxyResolutionTelemetryEvent {
297
373
  return {
298
374
  provider: "smartproxy",
375
+ outcome: "error",
299
376
  cacheStatus,
300
377
  cacheHit: false,
301
378
  resolutionMs: Math.max(0, Date.now() - startedAt),
@@ -365,70 +442,7 @@ function serializeSmartproxyPool(pool: CachedProxyPool): string {
365
442
 
366
443
  function normalizeProxyUrl(url?: string): string | undefined {
367
444
  const normalized = url?.trim();
368
- return normalized ? applyStickyProxySession(normalized) : undefined;
369
- }
370
-
371
- function readPositiveIntegerEnv(name: string): string | undefined {
372
- const raw = process.env[name]?.trim();
373
- if (!raw) return undefined;
374
- if (!/^[1-9]\d*$/.test(raw)) {
375
- throw new Error(`${name} must be a positive integer`);
376
- }
377
- return raw;
378
- }
379
-
380
- function applyStickyProxySession(proxyUrl: string): string {
381
- let parsed: URL;
382
- try {
383
- parsed = new URL(proxyUrl);
384
- } catch {
385
- return proxyUrl;
386
- }
387
-
388
- if (!parsed.hostname || !parsed.username || !parsed.password) {
389
- return proxyUrl;
390
- }
391
-
392
- const host = parsed.hostname.toLowerCase();
393
- if (!host.includes("smartproxy") && !host.includes("decodo")) {
394
- return proxyUrl;
395
- }
396
-
397
- const username = decodeURIComponent(parsed.username);
398
- const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
399
- const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
400
- const stickyUsername = host.includes("smartproxy")
401
- ? buildSmartproxyUsername(username, sessionId, sessionDuration)
402
- : buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
403
-
404
- parsed.username = stickyUsername;
405
- return parsed.toString();
406
- }
407
-
408
- function buildSmartproxyUsername(
409
- username: string,
410
- sessionId: string,
411
- sessionDuration?: string,
412
- ): string {
413
- const parts = username.split("_");
414
- const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
415
- const baseUsername = parts
416
- .filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
417
- .join("_");
418
- return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
419
- }
420
-
421
- function buildDecodoUsername(username: string, sessionId: string, sessionDuration: string): string {
422
- const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
423
- const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
424
- return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
425
- }
426
-
427
- function syncProxyEnv(config: ApiFuseConfig): void {
428
- const configProxyUrl = normalizeProxyUrl(config.proxy?.url);
429
- if (!process.env.APIFUSE__PROXY__URL && configProxyUrl) {
430
- process.env.APIFUSE__PROXY__URL = configProxyUrl;
431
- }
445
+ return normalized || undefined;
432
446
  }
433
447
 
434
448
  export function resolveProxyConfig(options: ProxyResolutionOptions = {}): ResolvedProxyConfig {
@@ -448,16 +462,6 @@ export function resolveProxyConfig(options: ProxyResolutionOptions = {}): Resolv
448
462
  return { shouldWarn: false };
449
463
  }
450
464
 
451
- const envProxyUrl = normalizeProxyUrl(process.env.APIFUSE__PROXY__URL);
452
- if (envProxyUrl) {
453
- return { shouldWarn: false, url: envProxyUrl };
454
- }
455
-
456
- const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
457
- if (configuredProxyUrl) {
458
- return { shouldWarn: false, url: configuredProxyUrl };
459
- }
460
-
461
465
  return { shouldWarn: true };
462
466
  }
463
467
 
@@ -477,60 +481,285 @@ export async function resolveProxyConfigAsync(
477
481
  return { shouldWarn: false };
478
482
  }
479
483
 
480
- const provider = resolveProxyProvider(policy);
481
- if (provider !== "smartproxy") {
484
+ const chain = resolveVendorChain(policy);
485
+ if (chain.length === 0) {
486
+ const declared = declaredVendorChain(policy);
487
+ const deprecated = declared.filter((vendor) => vendor === "decodo" || vendor === "custom");
488
+ if (policy.mode === "required") {
489
+ const providerIds =
490
+ declared.length > 0 ? declared.map((vendor) => `"${vendor}"`).join(", ") : "none";
491
+ const deprecatedDetail =
492
+ deprecated.length > 0
493
+ ? ` Deprecated vendor(s): ${deprecated.map((vendor) => `"${vendor}"`).join(", ")}.`
494
+ : "";
495
+ throw new ProxyResolutionError(
496
+ "PROXY_REQUIRED",
497
+ `Required proxy policy has no SDK-managed adapter for provider id(s): ${providerIds}.${deprecatedDetail} Use "smartproxy" or "nodemaven".`,
498
+ );
499
+ }
500
+ // Deprecated decodo/custom providers have no SDK-managed adapter. Optional
501
+ // policies preserve the warning-only behavior and may continue directly.
482
502
  return resolveProxyConfig({
483
503
  ...options,
484
504
  upstream: { proxy: true },
485
505
  });
486
506
  }
487
507
 
488
- const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
489
- if (!appKey) {
490
- if (policy.mode === "required") {
508
+ // Protocol is chosen per vendor (each vendor's benchmarked-best), with an
509
+ // optional explicit override for the harness/tests. Both are tunnelling
510
+ // schemes. transportProtocols is what the calling transport can actually use.
511
+ const transportProtocols = options.transportProtocols ?? (["http", "socks5"] as const);
512
+
513
+ const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
514
+ const total = sizes.reduce((sum, size) => sum + size, 0);
515
+ const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
516
+ const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(
517
+ total > 0 ? normalizedAttempt % total : 0,
518
+ sizes,
519
+ );
520
+ const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
521
+
522
+ let lastError: unknown;
523
+ let blockedProtocol: ProxyProtocol | undefined;
524
+ for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
525
+ const vendor = chain[vendorIndex] as ProxyVendorName;
526
+ const nextVendor = chain[vendorIndex + 1];
527
+ const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
528
+ const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
529
+
530
+ if (!vendorHasCredentials(vendor)) {
531
+ options.telemetry?.recordProxyVendorFailover?.({
532
+ vendor,
533
+ nextVendor,
534
+ phase: "resolution",
535
+ reason: "no_credentials",
536
+ });
537
+ continue;
538
+ }
539
+
540
+ // The calling transport must be able to use this vendor's protocol; if not,
541
+ // fail over to the next vendor rather than silently downgrading.
542
+ if (!transportProtocols.includes(protocol)) {
543
+ blockedProtocol = protocol;
544
+ options.telemetry?.recordProxyVendorFailover?.({
545
+ vendor,
546
+ nextVendor,
547
+ phase: "resolution",
548
+ reason: "protocol_unsupported",
549
+ });
550
+ continue;
551
+ }
552
+
553
+ try {
554
+ return await resolveWithVendor(vendor, policy, options, {
555
+ protocol,
556
+ poolIndex,
557
+ refreshEpoch,
558
+ });
559
+ } catch (error) {
560
+ // Config/programming errors (invalid filter, etc.) are not vendor
561
+ // outages — propagate them rather than failing over.
562
+ if (!(error instanceof ProxyResolutionError)) {
563
+ throw error;
564
+ }
565
+ if (error.telemetry) {
566
+ options.telemetry?.recordProxyResolution(error.telemetry);
567
+ }
568
+ lastError = error;
569
+ options.telemetry?.recordProxyVendorFailover?.({
570
+ vendor,
571
+ nextVendor,
572
+ phase: "resolution",
573
+ reason: "allocation_failed",
574
+ });
575
+ }
576
+ }
577
+
578
+ if (policy.mode === "required") {
579
+ if (lastError) {
580
+ throw lastError instanceof ProxyResolutionError
581
+ ? lastError
582
+ : new ProxyResolutionError(
583
+ "PROXY_ALLOCATION_FAILED",
584
+ `All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`,
585
+ { cause: lastError, vendorChain: chain },
586
+ );
587
+ }
588
+ if (blockedProtocol) {
491
589
  throw new ProxyResolutionError(
492
- "PROXY_REQUIRED",
493
- `Smartproxy egress is required but ${SMARTPROXY_APP_KEY_ENV} is not configured.`,
590
+ "PROXY_PROTOCOL_UNSUPPORTED",
591
+ `No proxy vendor in [${chain.join(", ")}] could serve a protocol supported by this transport (supports: ${transportProtocols.join(", ")}; vendor wanted "${blockedProtocol}"). Route this provider through the stealth transport.`,
592
+ { protocol: blockedProtocol, vendorChain: chain },
494
593
  );
495
594
  }
496
- return { shouldWarn: true };
595
+ throw new ProxyResolutionError(
596
+ "PROXY_REQUIRED",
597
+ `Proxy egress is required but no vendor credentials are configured. Missing: ${chain
598
+ .map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
599
+ .join(", ")}.`,
600
+ { vendorChain: chain },
601
+ );
497
602
  }
498
- const lifetimeMinutes = resolveSmartproxyLifetime(policy);
603
+ return { shouldWarn: true };
604
+ }
499
605
 
606
+ /**
607
+ * Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
608
+ * Vendor allocation and failover remain owned by the SDK.
609
+ */
610
+ export async function resolveProxy(
611
+ options: ProxyResolutionOptions = {},
612
+ ): Promise<ResolvedProxyConfig> {
613
+ const resolved = await resolveProxyConfigAsync(options);
614
+ const vendor = vendorFromResolvedSource(resolved.source);
615
+ return vendor ? { ...resolved, vendor } : resolved;
616
+ }
617
+
618
+ /**
619
+ * Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
620
+ * CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
621
+ * for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
622
+ * Override per call via ProxyResolutionOptions.protocol (harness/tests).
623
+ */
624
+ export const VENDOR_DEFAULT_PROTOCOL: Readonly<Record<ProxyVendorName, ProxyProtocol>> = {
625
+ smartproxy: "http",
626
+ nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
627
+ };
628
+
629
+ /**
630
+ * Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
631
+ * (http CONNECT or socks5) so the client TLS handshake reaches the origin
632
+ * end-to-end. Anything else would intercept TLS and break fingerprinting.
633
+ */
634
+ export function assertTunnelingScheme(url: string): void {
635
+ let scheme: string;
500
636
  try {
501
- const allocated = await allocateSmartproxy(
502
- policy,
503
- appKey,
504
- lifetimeMinutes,
505
- options.affinityKey,
637
+ scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
638
+ } catch {
639
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
640
+ }
641
+ if (scheme !== "http" && scheme !== "socks5") {
642
+ throw new ProxyResolutionError(
643
+ "PROXY_ALLOCATION_FAILED",
644
+ `Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`,
506
645
  );
507
- options.telemetry?.recordProxyResolution(allocated.telemetry);
508
- const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, options.proxyAttempt);
646
+ }
647
+ }
648
+
649
+ export type ProxyVendorResolutionContext = {
650
+ readonly protocol: ProxyProtocol;
651
+ readonly poolIndex: number;
652
+ readonly refreshEpoch: number;
653
+ /** Explicit vendor credentials. Omit only on the legacy ambient-env path. */
654
+ readonly credentials?: Readonly<Record<string, string>>;
655
+ /** Disable non-policy env defaults for deterministic injected adapters. */
656
+ readonly ambientDefaults?: boolean;
657
+ /** Disable env-discovered Redis sharing for deterministic injected adapters. */
658
+ readonly sharedCache?: boolean;
659
+ };
660
+
661
+ export async function resolveWithVendor(
662
+ vendor: ProxyVendorName,
663
+ policy: ProviderProxyPolicy,
664
+ options: ProxyResolutionOptions,
665
+ context: ProxyVendorResolutionContext,
666
+ ): Promise<ResolvedProxyConfig> {
667
+ if (vendor === "nodemaven") {
668
+ const startedAt = Date.now();
669
+ const username = (
670
+ context.credentials === undefined
671
+ ? process.env[NODEMAVEN_USERNAME_ENV]
672
+ : context.credentials[NODEMAVEN_USERNAME_ENV]
673
+ )?.trim();
674
+ const password = (
675
+ context.credentials === undefined
676
+ ? process.env[NODEMAVEN_PASSWORD_ENV]
677
+ : context.credentials[NODEMAVEN_PASSWORD_ENV]
678
+ )?.trim();
679
+ const filter =
680
+ context.credentials === undefined
681
+ ? process.env[NODEMAVEN_FILTER_ENV]
682
+ : context.credentials[NODEMAVEN_FILTER_ENV];
683
+ if (!username || !password) {
684
+ throw new ProxyResolutionError(
685
+ "PROXY_ALLOCATION_FAILED",
686
+ `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
687
+ { vendor: "nodemaven" },
688
+ );
689
+ }
690
+ const synthesized = synthesizeNodemavenProxy({
691
+ policy,
692
+ credentials: {
693
+ username,
694
+ password,
695
+ ...(filter ? { filter } : {}),
696
+ },
697
+ affinityKey: options.affinityKey,
698
+ protocol: context.protocol,
699
+ poolIndex: context.poolIndex,
700
+ refreshEpoch: context.refreshEpoch,
701
+ country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
702
+ });
703
+ options.telemetry?.recordProxyResolution({
704
+ provider: "nodemaven",
705
+ protocol: synthesized.protocol,
706
+ cacheStatus: "disabled",
707
+ cacheHit: false,
708
+ resolutionMs: Math.max(0, Date.now() - startedAt),
709
+ attempts: 1,
710
+ });
711
+ assertTunnelingScheme(synthesized.url);
509
712
  return {
510
713
  shouldWarn: false,
511
- url: allocated.pool.urls[poolIndex],
512
- source: "smartproxy-allocator",
714
+ url: synthesized.url,
715
+ source: "nodemaven-gateway",
716
+ protocol: synthesized.protocol,
513
717
  diagnostics: {
514
- ...allocated.pool.diagnostics,
515
- poolSize: allocated.pool.urls.length,
516
- poolIndex,
718
+ ...synthesized.diagnostics,
719
+ poolIndex: context.poolIndex,
517
720
  },
518
721
  };
519
- } catch (error) {
520
- if (error instanceof ProxyResolutionError && error.telemetry) {
521
- options.telemetry?.recordProxyResolution(error.telemetry);
522
- }
523
- if (policy.mode === "required") {
524
- throw error instanceof ProxyResolutionError
525
- ? error
526
- : new ProxyResolutionError(
527
- "PROXY_ALLOCATION_FAILED",
528
- "Smartproxy allocator failed for required proxy egress.",
529
- { cause: error },
530
- );
531
- }
532
- return { shouldWarn: true };
533
722
  }
723
+
724
+ // smartproxy allocation-style vendor.
725
+ const appKey = (
726
+ context.credentials === undefined
727
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
728
+ : context.credentials[SMARTPROXY_APP_KEY_ENV]
729
+ )?.trim();
730
+ if (!appKey) {
731
+ // Guarded by vendorHasCredentials; treated as a vendor-internal failure.
732
+ throw new ProxyResolutionError(
733
+ "PROXY_ALLOCATION_FAILED",
734
+ `${SMARTPROXY_APP_KEY_ENV} is not configured.`,
735
+ { vendor: "smartproxy" },
736
+ );
737
+ }
738
+ const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
739
+ const allocated = await allocateSmartproxy(
740
+ policy,
741
+ appKey,
742
+ lifetimeMinutes,
743
+ options.affinityKey,
744
+ context.protocol,
745
+ context.ambientDefaults !== false,
746
+ context.sharedCache !== false,
747
+ );
748
+ options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
749
+ const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
750
+ const url = allocated.pool.urls[poolIndex];
751
+ if (url) assertTunnelingScheme(url);
752
+ return {
753
+ shouldWarn: false,
754
+ url,
755
+ source: "smartproxy-allocator",
756
+ protocol: context.protocol,
757
+ diagnostics: {
758
+ ...allocated.pool.diagnostics,
759
+ poolSize: allocated.pool.urls.length,
760
+ poolIndex,
761
+ },
762
+ };
534
763
  }
535
764
 
536
765
  function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | undefined {
@@ -544,21 +773,221 @@ function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | u
544
773
  return undefined;
545
774
  }
546
775
 
547
- function resolveProxyProvider(policy: ProviderProxyPolicy): string {
548
- return (
549
- policy.provider ?? process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ?? "custom"
776
+ function isRegistryVendor(name: string | undefined): name is ProxyVendorName {
777
+ return name === "smartproxy" || name === "nodemaven";
778
+ }
779
+
780
+ function declaredVendorChain(policy: ProviderProxyPolicy): ProviderProxyProvider[] {
781
+ const declared = policy.providers?.length
782
+ ? policy.providers
783
+ : [policy.provider ?? envDefaultProvider()];
784
+ return declared.filter((vendor): vendor is ProviderProxyProvider => vendor !== undefined);
785
+ }
786
+
787
+ /**
788
+ * Ordered list of SDK-native proxy vendors declared by the policy. `providers`
789
+ * takes precedence over the legacy singular `provider`; the platform default
790
+ * env is the final fallback. Non-registry names (decodo/custom) are dropped so
791
+ * an all-deprecated chain has no managed adapter.
792
+ */
793
+ export function resolveVendorChain(policy: ProviderProxyPolicy): ProxyVendorName[] {
794
+ const chain: ProxyVendorName[] = [];
795
+ for (const name of declaredVendorChain(policy)) {
796
+ if (isRegistryVendor(name) && !chain.includes(name)) {
797
+ chain.push(name);
798
+ }
799
+ }
800
+ return chain;
801
+ }
802
+
803
+ function envDefaultProvider(): ProviderProxyProvider | undefined {
804
+ const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
805
+ return (raw as ProviderProxyProvider | undefined) ?? undefined;
806
+ }
807
+
808
+ function vendorHasCredentials(vendor: ProxyVendorName): boolean {
809
+ if (vendor === "nodemaven") return hasNodemavenCredentials();
810
+ return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
811
+ }
812
+
813
+ function missingCredentialEnv(vendor: ProxyVendorName): string {
814
+ return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
815
+ }
816
+
817
+ function vendorPoolSize(vendor: ProxyVendorName, policy: ProviderProxyPolicy): number {
818
+ return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
819
+ }
820
+
821
+ /**
822
+ * Total attempt span across a policy's vendor chain — the sum of each vendor's
823
+ * pool size. Transports use this so successive attempts rotate a vendor's pool
824
+ * and then fail over to the next vendor via the flat attempt index. With one
825
+ * vendor this equals that vendor's pool size (today's behaviour).
826
+ */
827
+ export function resolvePolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
828
+ const chain = resolveVendorChain(policy);
829
+ if (chain.length === 0) return resolveSmartproxyPoolSize(policy);
830
+ return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
831
+ }
832
+
833
+ function vendorMaxPoolSize(vendor: ProxyVendorName): number {
834
+ return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
835
+ }
836
+
837
+ /**
838
+ * Absolute upper bound on a chain's attempt span — the sum of each vendor's
839
+ * *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
840
+ * span), this backstop is independent of `session.poolSize`, so it never
841
+ * truncates a legitimately large pool below the point where the flat attempt
842
+ * index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
843
+ */
844
+ export function maxPolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
845
+ const chain = resolveVendorChain(policy);
846
+ if (chain.length === 0) return SMARTPROXY_MAX_POOL_SIZE;
847
+ return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
848
+ }
849
+
850
+ const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
851
+
852
+ /**
853
+ * Transport-retry attempt cap for a policy-managed request. A transport failure
854
+ * rotates the flat attempt index onto the *next* endpoint (and, once the index
855
+ * passes the primary vendor's pool span, the *next vendor*), so the cap must be
856
+ * the chain's full pool span for failover to reach the fallback vendor — the
857
+ * per-endpoint retry budget (default 3) never gets there.
858
+ *
859
+ * The span only widens beyond the caller's retry budget when ALL hold:
860
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
861
+ * - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
862
+ * is the documented total-attempt ceiling and must be honoured verbatim;
863
+ * - the method is safe/idempotent — an unsafe request must never be duplicated
864
+ * across the pool even if some framework default would allow it;
865
+ * - the policy resolves a non-empty *registry* vendor chain (smartproxy /
866
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed pool,
867
+ * so there is no possible endpoint crossover — they keep the retry budget.
868
+ *
869
+ * The widened cap is bounded by the chain's true maximum span (sum of each
870
+ * vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
871
+ * a pathological chain can never spin unbounded.
872
+ */
873
+ /**
874
+ * True when a policy request is in *implicit chain-rotation* mode: successive
875
+ * transport attempts rotate the flat index across the concatenated vendor pool
876
+ * spans (and, past the primary vendor's span, into the fallback vendor). This is
877
+ * the ONLY mode in which the transport loop widens its attempt cap AND
878
+ * de-duplicates repeated endpoints — the two behaviours must share one predicate
879
+ * so they never diverge. It holds when ALL of the widening conditions hold:
880
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
881
+ * - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
882
+ * the documented contract and must be honoured verbatim against whatever
883
+ * endpoint each attempt resolves (even a repeated one), so no de-duplication;
884
+ * - the method is safe/idempotent — an unsafe request is never duplicated;
885
+ * - the policy resolves a non-empty registry vendor chain (smartproxy /
886
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed
887
+ * endpoint, so there is nothing to rotate or de-duplicate.
888
+ */
889
+ export function policyRotatesTransportVendorChain(input: {
890
+ policy: ProviderProxyPolicy | undefined;
891
+ usesPolicyAllocator: boolean;
892
+ explicitRetry: boolean;
893
+ method: string;
894
+ }): boolean {
895
+ if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
896
+ return false;
897
+ }
898
+ if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
899
+ return false;
900
+ }
901
+ return resolveVendorChain(input.policy).length > 0;
902
+ }
903
+
904
+ export function resolvePolicyTransportAttemptCap(input: {
905
+ policy: ProviderProxyPolicy | undefined;
906
+ usesPolicyAllocator: boolean;
907
+ retryAttempts: number;
908
+ explicitRetry: boolean;
909
+ method: string;
910
+ }): number {
911
+ const budget = Math.max(1, Math.floor(input.retryAttempts));
912
+ if (
913
+ !policyRotatesTransportVendorChain({
914
+ policy: input.policy,
915
+ usesPolicyAllocator: input.usesPolicyAllocator,
916
+ explicitRetry: input.explicitRetry,
917
+ method: input.method,
918
+ })
919
+ ) {
920
+ return budget;
921
+ }
922
+ const span = Math.min(
923
+ maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
924
+ resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
550
925
  );
926
+ return Math.max(budget, span);
927
+ }
928
+
929
+ /**
930
+ * A registry vendor chain (smartproxy/nodemaven) resolves a potentially
931
+ * *different* endpoint per flat attempt index, so a transport retry should
932
+ * advance across endpoints and de-duplicate once the chain stops yielding new
933
+ * ones. Deprecated custom/decodo policies have an empty registry chain and no
934
+ * managed endpoint, so the transport loop has nothing to rotate or de-duplicate.
935
+ */
936
+ export function policyResolvesRegistryVendorChain(
937
+ policy: ProviderProxyPolicy | undefined,
938
+ ): boolean {
939
+ return Boolean(policy) && resolveVendorChain(policy as ProviderProxyPolicy).length > 0;
940
+ }
941
+
942
+ /** Map a resolved proxy source label to the vendor that served it. */
943
+ export function vendorFromResolvedSource(
944
+ source: ResolvedProxyConfig["source"],
945
+ ): ProxyVendorName | undefined {
946
+ if (source === "nodemaven-gateway") return "nodemaven";
947
+ if (source === "smartproxy-allocator") return "smartproxy";
948
+ return undefined;
949
+ }
950
+
951
+ function normalizeAttemptIndex(attempt: number | undefined): number {
952
+ return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt as number)) : 0;
953
+ }
954
+
955
+ /**
956
+ * Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
957
+ * vendor's pool space in chain order. With a single vendor this reduces to
958
+ * `attempt % poolSize`, preserving today's behaviour exactly.
959
+ */
960
+ export function mapFlatAttempt(
961
+ flat: number,
962
+ sizes: readonly number[],
963
+ ): { vendorIndex: number; poolIndex: number } {
964
+ let cursor = flat;
965
+ for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
966
+ const size = Math.max(1, sizes[vendorIndex] ?? 1);
967
+ if (cursor < size) {
968
+ return { vendorIndex, poolIndex: cursor };
969
+ }
970
+ cursor -= size;
971
+ }
972
+ return { vendorIndex: 0, poolIndex: 0 };
551
973
  }
552
974
 
553
- function resolveSmartproxyCountry(policy: ProviderProxyPolicy): string | undefined {
975
+ function resolveSmartproxyCountry(
976
+ policy: ProviderProxyPolicy,
977
+ ambientDefaults = true,
978
+ ): string | undefined {
554
979
  return (
555
- policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined
980
+ policy.geo?.country ??
981
+ (ambientDefaults
982
+ ? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
983
+ : undefined)
556
984
  );
557
985
  }
558
986
 
559
- function resolveSmartproxyLifetime(policy: ProviderProxyPolicy): number {
987
+ function resolveSmartproxyLifetime(policy: ProviderProxyPolicy, ambientDefaults = true): number {
560
988
  const configuredLifetime =
561
- policy.session?.lifetimeMinutes ?? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
989
+ policy.session?.lifetimeMinutes ??
990
+ (ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
562
991
  return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
563
992
  }
564
993
 
@@ -589,13 +1018,21 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
589
1018
 
590
1019
  function buildSmartproxyCacheKey(
591
1020
  policy: ProviderProxyPolicy,
1021
+ appKey: string,
592
1022
  affinityKey: string | undefined,
593
1023
  lifetimeMinutes: number,
1024
+ protocol: ProxyProtocol,
1025
+ ambientDefaults = true,
594
1026
  ): string {
595
1027
  const poolSize = resolveSmartproxyPoolSize(policy);
596
1028
  return JSON.stringify({
597
1029
  provider: "smartproxy",
598
- country: resolveSmartproxyCountry(policy),
1030
+ credentialHash: createHash("sha256")
1031
+ .update("apifuse-smartproxy-credential:v1\0")
1032
+ .update(appKey)
1033
+ .digest("hex"),
1034
+ protocol,
1035
+ country: resolveSmartproxyCountry(policy, ambientDefaults),
599
1036
  affinity: policy.session?.affinity ?? "request",
600
1037
  affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
601
1038
  lifetimeMinutes,
@@ -608,8 +1045,18 @@ async function allocateSmartproxy(
608
1045
  appKey: string,
609
1046
  lifetimeMinutes: number,
610
1047
  affinityKey: string | undefined,
1048
+ protocol: ProxyProtocol,
1049
+ ambientDefaults = true,
1050
+ sharedCache = true,
611
1051
  ): Promise<SmartproxyAllocationResult> {
612
- const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
1052
+ const cacheKey = buildSmartproxyCacheKey(
1053
+ policy,
1054
+ appKey,
1055
+ affinityKey,
1056
+ lifetimeMinutes,
1057
+ protocol,
1058
+ ambientDefaults,
1059
+ );
613
1060
  const startedAt = Date.now();
614
1061
  const now = startedAt;
615
1062
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
@@ -617,7 +1064,15 @@ async function allocateSmartproxy(
617
1064
  const cached = proxyCache.get(cacheKey);
618
1065
  if (!skipCached && cached && isFresh(cached, now)) {
619
1066
  if (shouldSoftRefresh(cached, now)) {
620
- void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
1067
+ void refreshSmartproxyPool(
1068
+ cacheKey,
1069
+ policy,
1070
+ appKey,
1071
+ lifetimeMinutes,
1072
+ protocol,
1073
+ ambientDefaults,
1074
+ sharedCache,
1075
+ );
621
1076
  return {
622
1077
  pool: cached,
623
1078
  telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
@@ -631,7 +1086,7 @@ async function allocateSmartproxy(
631
1086
  };
632
1087
  }
633
1088
 
634
- if (!skipCached) {
1089
+ if (!skipCached && sharedCache) {
635
1090
  const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
636
1091
  if (redisResult) return redisResult;
637
1092
  }
@@ -653,6 +1108,9 @@ async function allocateSmartproxy(
653
1108
  appKey,
654
1109
  lifetimeMinutes,
655
1110
  startedAt,
1111
+ protocol,
1112
+ ambientDefaults,
1113
+ sharedCache,
656
1114
  ).finally(() => {
657
1115
  proxyInflight.delete(cacheKey);
658
1116
  });
@@ -691,11 +1149,24 @@ async function refreshSmartproxyPool(
691
1149
  policy: ProviderProxyPolicy,
692
1150
  appKey: string,
693
1151
  lifetimeMinutes: number,
1152
+ protocol: ProxyProtocol,
1153
+ ambientDefaults: boolean,
1154
+ sharedCache: boolean,
694
1155
  ): Promise<void> {
695
1156
  try {
696
- await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
697
- background: true,
698
- });
1157
+ await allocateSmartproxyShared(
1158
+ cacheKey,
1159
+ policy,
1160
+ appKey,
1161
+ lifetimeMinutes,
1162
+ Date.now(),
1163
+ protocol,
1164
+ ambientDefaults,
1165
+ sharedCache,
1166
+ {
1167
+ background: true,
1168
+ },
1169
+ );
699
1170
  } catch {
700
1171
  // Soft refresh is opportunistic; current fresh pool remains usable.
701
1172
  }
@@ -707,9 +1178,12 @@ async function allocateSmartproxyShared(
707
1178
  appKey: string,
708
1179
  lifetimeMinutes: number,
709
1180
  startedAt: number,
1181
+ protocol: ProxyProtocol,
1182
+ ambientDefaults: boolean,
1183
+ sharedCache: boolean,
710
1184
  options: { background?: boolean } = {},
711
1185
  ): Promise<SmartproxyAllocationResult> {
712
- const redis = getProxyRedis();
1186
+ const redis = sharedCache ? getProxyRedis() : undefined;
713
1187
  if (!redis || !(await ensureRedisReady(redis))) {
714
1188
  return await allocateAndStoreSmartproxyPool(
715
1189
  cacheKey,
@@ -717,7 +1191,7 @@ async function allocateSmartproxyShared(
717
1191
  appKey,
718
1192
  lifetimeMinutes,
719
1193
  startedAt,
720
- { cacheStatus: "allocator" },
1194
+ { cacheStatus: "allocator", protocol, ambientDefaults },
721
1195
  );
722
1196
  }
723
1197
 
@@ -742,6 +1216,8 @@ async function allocateSmartproxyShared(
742
1216
  cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
743
1217
  redis,
744
1218
  poolKey,
1219
+ protocol,
1220
+ ambientDefaults,
745
1221
  },
746
1222
  );
747
1223
  } finally {
@@ -897,10 +1373,19 @@ async function allocateAndStoreSmartproxyPool(
897
1373
  cacheStatus: ProxyCacheStatus;
898
1374
  redis?: ProxyRedisClient;
899
1375
  poolKey?: string;
1376
+ protocol: ProxyProtocol;
1377
+ ambientDefaults: boolean;
900
1378
  },
901
1379
  ): Promise<SmartproxyAllocationResult> {
902
1380
  const poolSize = resolveSmartproxyPoolSize(policy);
903
- const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
1381
+ const allocatorUrl = buildSmartproxyAllocatorUrl(
1382
+ policy,
1383
+ appKey,
1384
+ lifetimeMinutes,
1385
+ poolSize,
1386
+ options.protocol,
1387
+ options.ambientDefaults,
1388
+ );
904
1389
  const allocatorStartedAt = Date.now();
905
1390
  const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
906
1391
  let allocation: SmartproxyAllocatorSuccess | undefined;
@@ -914,6 +1399,7 @@ async function allocateAndStoreSmartproxyPool(
914
1399
  allocatorUrl,
915
1400
  attempt,
916
1401
  allocatorDeadlineAt,
1402
+ options.protocol,
917
1403
  );
918
1404
  if (attemptResult.ok) {
919
1405
  allocation = attemptResult;
@@ -969,7 +1455,7 @@ async function allocateAndStoreSmartproxyPool(
969
1455
  expiresAt: allocatedAt + ttlMs,
970
1456
  diagnostics: {
971
1457
  provider: "smartproxy",
972
- country: resolveSmartproxyCountry(policy) ?? "default",
1458
+ country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
973
1459
  lifetimeMinutes,
974
1460
  affinity: policy.session?.affinity ?? "request",
975
1461
  rawConnect: true,
@@ -1034,6 +1520,7 @@ async function fetchSmartproxyAllocatorAttempt(
1034
1520
  allocatorUrl: string,
1035
1521
  attempt: number,
1036
1522
  deadlineAt: number,
1523
+ protocol: ProxyProtocol,
1037
1524
  ): Promise<SmartproxyAllocatorAttemptResult> {
1038
1525
  const { controller, dispose } = createDeadlineAbortController(deadlineAt);
1039
1526
  let response: Response;
@@ -1077,7 +1564,7 @@ async function fetchSmartproxyAllocatorAttempt(
1077
1564
  };
1078
1565
  }
1079
1566
 
1080
- const urls = parseSmartproxyAllocatorProxies(body);
1567
+ const urls = parseSmartproxyAllocatorProxies(body, protocol);
1081
1568
  const bodyClass = classifySmartproxyAllocatorBody(body, urls);
1082
1569
  if (urls.length === 0) {
1083
1570
  return {
@@ -1117,22 +1604,32 @@ function smartproxyAllocatorFailureMessage(
1117
1604
  return "Smartproxy allocator response did not contain a usable proxy endpoint.";
1118
1605
  }
1119
1606
 
1607
+ // Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
1608
+ // best-effort mapping pending live vendor confirmation; http is the default and
1609
+ // the only value exercised in production today.
1610
+ const SMARTPROXY_PROTOCOL_PARAM: Record<ProxyProtocol, string> = {
1611
+ http: "1",
1612
+ socks5: "2",
1613
+ };
1614
+
1120
1615
  function buildSmartproxyAllocatorUrl(
1121
1616
  policy: ProviderProxyPolicy,
1122
1617
  appKey: string,
1123
1618
  lifetimeMinutes: number,
1124
1619
  poolSize: number,
1620
+ protocol: ProxyProtocol,
1621
+ ambientDefaults = true,
1125
1622
  ): string {
1126
1623
  const params = new URLSearchParams({
1127
1624
  app_key: appKey,
1128
1625
  pt: "9",
1129
1626
  num: String(poolSize),
1130
1627
  life: String(lifetimeMinutes),
1131
- protocol: "1",
1628
+ protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
1132
1629
  format: "txt",
1133
1630
  lb: "\\n",
1134
1631
  });
1135
- const country = resolveSmartproxyCountry(policy);
1632
+ const country = resolveSmartproxyCountry(policy, ambientDefaults);
1136
1633
  if (country) {
1137
1634
  params.set("cc", country);
1138
1635
  }
@@ -1141,7 +1638,8 @@ function buildSmartproxyAllocatorUrl(
1141
1638
  return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
1142
1639
  }
1143
1640
 
1144
- function parseSmartproxyAllocatorProxies(body: string): string[] {
1641
+ function parseSmartproxyAllocatorProxies(body: string, protocol: ProxyProtocol): string[] {
1642
+ const scheme = protocol === "socks5" ? "socks5" : "http";
1145
1643
  const trimmed = body.trim();
1146
1644
  if (!trimmed) {
1147
1645
  return [];
@@ -1162,7 +1660,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
1162
1660
  "port" in item && (typeof item.port === "string" || typeof item.port === "number")
1163
1661
  ? item.port
1164
1662
  : "";
1165
- return ip && port ? `http://${ip}:${port}` : null;
1663
+ return ip && port ? `${scheme}://${ip}:${port}` : null;
1166
1664
  })
1167
1665
  .filter((url): url is string => url !== null);
1168
1666
  }
@@ -1174,7 +1672,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
1174
1672
  .split(/\r?\n/)
1175
1673
  .map((item) => item.trim())
1176
1674
  .filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
1177
- .map((line) => `http://${line}`);
1675
+ .map((line) => `${scheme}://${line}`);
1178
1676
  }
1179
1677
 
1180
1678
  function classifySmartproxyAllocatorBody(
@@ -1207,12 +1705,20 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
1207
1705
  if (!policy || policy.mode === "disabled") {
1208
1706
  return undefined;
1209
1707
  }
1210
- if (resolveProxyProvider(policy) !== "smartproxy") {
1708
+ if (!resolveVendorChain(policy).includes("smartproxy")) {
1211
1709
  return undefined;
1212
1710
  }
1213
1711
 
1214
1712
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1215
- const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
1713
+ const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
1714
+ if (!appKey) return undefined;
1715
+ const cacheKey = buildSmartproxyCacheKey(
1716
+ policy,
1717
+ appKey,
1718
+ options.affinityKey,
1719
+ lifetimeMinutes,
1720
+ options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
1721
+ );
1216
1722
  invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1217
1723
  proxyCache.delete(cacheKey);
1218
1724
  proxyInflight.delete(cacheKey);
@@ -1262,17 +1768,13 @@ export async function loadApiFuseConfig(dir: string = process.cwd()): Promise<Ap
1262
1768
  const tsPath = path.resolve(dir, "apifuse.config.ts");
1263
1769
  if (existsSync(tsPath)) {
1264
1770
  const config = await importConfig(tsPath);
1265
- const resolvedConfig = config ?? {};
1266
- syncProxyEnv(resolvedConfig);
1267
- return resolvedConfig;
1771
+ return config ?? {};
1268
1772
  }
1269
1773
 
1270
1774
  const jsPath = path.resolve(dir, "apifuse.config.js");
1271
1775
  if (existsSync(jsPath)) {
1272
1776
  const config = await importConfig(jsPath);
1273
- const resolvedConfig = config ?? {};
1274
- syncProxyEnv(resolvedConfig);
1275
- return resolvedConfig;
1777
+ return config ?? {};
1276
1778
  }
1277
1779
 
1278
1780
  return {};