@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,7 +1,12 @@
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
- import { Redis } from "ioredis";
5
+ import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, hasNodemavenCredentials, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
6
+ // "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
7
+ // extraction API (app_key → raw ip:port pool). It is NOT the company formerly
8
+ // named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
9
+ // modelled separately as the `decodo` gateway vendor. Do not conflate them.
5
10
  export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
6
11
  export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
7
12
  export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
@@ -15,14 +20,21 @@ export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
15
20
  export class ProxyResolutionError extends Error {
16
21
  code;
17
22
  telemetry;
23
+ vendor;
24
+ vendorChain;
25
+ protocol;
18
26
  constructor(code, message, options) {
19
27
  super(message, options);
20
28
  this.name = "ProxyResolutionError";
21
29
  this.code = code;
22
30
  this.telemetry = options?.telemetry;
31
+ this.vendor = options?.vendor;
32
+ this.vendorChain = options?.vendorChain;
33
+ this.protocol = options?.protocol;
23
34
  }
24
35
  }
25
36
  const proxyCache = new Map();
37
+ const require = createRequire(import.meta.url);
26
38
  const proxyInflight = new Map();
27
39
  const invalidatedProxyKeys = new Map();
28
40
  const redisClients = new Map();
@@ -73,7 +85,8 @@ function getProxyRedis() {
73
85
  const existing = redisClients.get(redisUrl);
74
86
  if (existing)
75
87
  return existing;
76
- const redis = new Redis(redisUrl, {
88
+ const { Redis: RedisClient } = require("ioredis");
89
+ const redis = new RedisClient(redisUrl, {
77
90
  connectTimeout: REDIS_TIMEOUT_MS,
78
91
  enableOfflineQueue: false,
79
92
  lazyConnect: true,
@@ -145,6 +158,7 @@ function telemetryForPool(pool, cacheStatus, startedAt, extra = {}) {
145
158
  function telemetryForFailure(cacheStatus, startedAt, extra = {}) {
146
159
  return {
147
160
  provider: "smartproxy",
161
+ outcome: "error",
148
162
  cacheStatus,
149
163
  cacheHit: false,
150
164
  resolutionMs: Math.max(0, Date.now() - startedAt),
@@ -210,59 +224,7 @@ function serializeSmartproxyPool(pool) {
210
224
  }
211
225
  function normalizeProxyUrl(url) {
212
226
  const normalized = url?.trim();
213
- return normalized ? applyStickyProxySession(normalized) : undefined;
214
- }
215
- function readPositiveIntegerEnv(name) {
216
- const raw = process.env[name]?.trim();
217
- if (!raw)
218
- return undefined;
219
- if (!/^[1-9]\d*$/.test(raw)) {
220
- throw new Error(`${name} must be a positive integer`);
221
- }
222
- return raw;
223
- }
224
- function applyStickyProxySession(proxyUrl) {
225
- let parsed;
226
- try {
227
- parsed = new URL(proxyUrl);
228
- }
229
- catch {
230
- return proxyUrl;
231
- }
232
- if (!parsed.hostname || !parsed.username || !parsed.password) {
233
- return proxyUrl;
234
- }
235
- const host = parsed.hostname.toLowerCase();
236
- if (!host.includes("smartproxy") && !host.includes("decodo")) {
237
- return proxyUrl;
238
- }
239
- const username = decodeURIComponent(parsed.username);
240
- const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
241
- const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
242
- const stickyUsername = host.includes("smartproxy")
243
- ? buildSmartproxyUsername(username, sessionId, sessionDuration)
244
- : buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
245
- parsed.username = stickyUsername;
246
- return parsed.toString();
247
- }
248
- function buildSmartproxyUsername(username, sessionId, sessionDuration) {
249
- const parts = username.split("_");
250
- const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
251
- const baseUsername = parts
252
- .filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
253
- .join("_");
254
- return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
255
- }
256
- function buildDecodoUsername(username, sessionId, sessionDuration) {
257
- const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
258
- const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
259
- return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
260
- }
261
- function syncProxyEnv(config) {
262
- const configProxyUrl = normalizeProxyUrl(config.proxy?.url);
263
- if (!process.env.APIFUSE__PROXY__URL && configProxyUrl) {
264
- process.env.APIFUSE__PROXY__URL = configProxyUrl;
265
- }
227
+ return normalized || undefined;
266
228
  }
267
229
  export function resolveProxyConfig(options = {}) {
268
230
  const explicitProxyUrl = normalizeProxyUrl(options.proxy);
@@ -277,14 +239,6 @@ export function resolveProxyConfig(options = {}) {
277
239
  if (!legacyProxyRequested) {
278
240
  return { shouldWarn: false };
279
241
  }
280
- const envProxyUrl = normalizeProxyUrl(process.env.APIFUSE__PROXY__URL);
281
- if (envProxyUrl) {
282
- return { shouldWarn: false, url: envProxyUrl };
283
- }
284
- const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
285
- if (configuredProxyUrl) {
286
- return { shouldWarn: false, url: configuredProxyUrl };
287
- }
288
242
  return { shouldWarn: true };
289
243
  }
290
244
  export async function resolveProxyConfigAsync(options = {}) {
@@ -299,47 +253,211 @@ export async function resolveProxyConfigAsync(options = {}) {
299
253
  if (policy.mode === "disabled") {
300
254
  return { shouldWarn: false };
301
255
  }
302
- const provider = resolveProxyProvider(policy);
303
- if (provider !== "smartproxy") {
256
+ const chain = resolveVendorChain(policy);
257
+ if (chain.length === 0) {
258
+ const declared = declaredVendorChain(policy);
259
+ const deprecated = declared.filter((vendor) => vendor === "decodo" || vendor === "custom");
260
+ if (policy.mode === "required") {
261
+ const providerIds = declared.length > 0 ? declared.map((vendor) => `"${vendor}"`).join(", ") : "none";
262
+ const deprecatedDetail = deprecated.length > 0
263
+ ? ` Deprecated vendor(s): ${deprecated.map((vendor) => `"${vendor}"`).join(", ")}.`
264
+ : "";
265
+ throw new ProxyResolutionError("PROXY_REQUIRED", `Required proxy policy has no SDK-managed adapter for provider id(s): ${providerIds}.${deprecatedDetail} Use "smartproxy" or "nodemaven".`);
266
+ }
267
+ // Deprecated decodo/custom providers have no SDK-managed adapter. Optional
268
+ // policies preserve the warning-only behavior and may continue directly.
304
269
  return resolveProxyConfig({
305
270
  ...options,
306
271
  upstream: { proxy: true },
307
272
  });
308
273
  }
309
- const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
310
- if (!appKey) {
311
- if (policy.mode === "required") {
312
- throw new ProxyResolutionError("PROXY_REQUIRED", `Smartproxy egress is required but ${SMARTPROXY_APP_KEY_ENV} is not configured.`);
274
+ // Protocol is chosen per vendor (each vendor's benchmarked-best), with an
275
+ // optional explicit override for the harness/tests. Both are tunnelling
276
+ // schemes. transportProtocols is what the calling transport can actually use.
277
+ const transportProtocols = options.transportProtocols ?? ["http", "socks5"];
278
+ const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
279
+ const total = sizes.reduce((sum, size) => sum + size, 0);
280
+ const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
281
+ const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(total > 0 ? normalizedAttempt % total : 0, sizes);
282
+ const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
283
+ let lastError;
284
+ let blockedProtocol;
285
+ for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
286
+ const vendor = chain[vendorIndex];
287
+ const nextVendor = chain[vendorIndex + 1];
288
+ const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
289
+ const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
290
+ if (!vendorHasCredentials(vendor)) {
291
+ options.telemetry?.recordProxyVendorFailover?.({
292
+ vendor,
293
+ nextVendor,
294
+ phase: "resolution",
295
+ reason: "no_credentials",
296
+ });
297
+ continue;
298
+ }
299
+ // The calling transport must be able to use this vendor's protocol; if not,
300
+ // fail over to the next vendor rather than silently downgrading.
301
+ if (!transportProtocols.includes(protocol)) {
302
+ blockedProtocol = protocol;
303
+ options.telemetry?.recordProxyVendorFailover?.({
304
+ vendor,
305
+ nextVendor,
306
+ phase: "resolution",
307
+ reason: "protocol_unsupported",
308
+ });
309
+ continue;
310
+ }
311
+ try {
312
+ return await resolveWithVendor(vendor, policy, options, {
313
+ protocol,
314
+ poolIndex,
315
+ refreshEpoch,
316
+ });
317
+ }
318
+ catch (error) {
319
+ // Config/programming errors (invalid filter, etc.) are not vendor
320
+ // outages — propagate them rather than failing over.
321
+ if (!(error instanceof ProxyResolutionError)) {
322
+ throw error;
323
+ }
324
+ if (error.telemetry) {
325
+ options.telemetry?.recordProxyResolution(error.telemetry);
326
+ }
327
+ lastError = error;
328
+ options.telemetry?.recordProxyVendorFailover?.({
329
+ vendor,
330
+ nextVendor,
331
+ phase: "resolution",
332
+ reason: "allocation_failed",
333
+ });
313
334
  }
314
- return { shouldWarn: true };
315
335
  }
316
- const lifetimeMinutes = resolveSmartproxyLifetime(policy);
336
+ if (policy.mode === "required") {
337
+ if (lastError) {
338
+ throw lastError instanceof ProxyResolutionError
339
+ ? lastError
340
+ : new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`, { cause: lastError, vendorChain: chain });
341
+ }
342
+ if (blockedProtocol) {
343
+ throw new ProxyResolutionError("PROXY_PROTOCOL_UNSUPPORTED", `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.`, { protocol: blockedProtocol, vendorChain: chain });
344
+ }
345
+ throw new ProxyResolutionError("PROXY_REQUIRED", `Proxy egress is required but no vendor credentials are configured. Missing: ${chain
346
+ .map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
347
+ .join(", ")}.`, { vendorChain: chain });
348
+ }
349
+ return { shouldWarn: true };
350
+ }
351
+ /**
352
+ * Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
353
+ * Vendor allocation and failover remain owned by the SDK.
354
+ */
355
+ export async function resolveProxy(options = {}) {
356
+ const resolved = await resolveProxyConfigAsync(options);
357
+ const vendor = vendorFromResolvedSource(resolved.source);
358
+ return vendor ? { ...resolved, vendor } : resolved;
359
+ }
360
+ /**
361
+ * Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
362
+ * CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
363
+ * for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
364
+ * Override per call via ProxyResolutionOptions.protocol (harness/tests).
365
+ */
366
+ export const VENDOR_DEFAULT_PROTOCOL = {
367
+ smartproxy: "http",
368
+ nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
369
+ };
370
+ /**
371
+ * Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
372
+ * (http CONNECT or socks5) so the client TLS handshake reaches the origin
373
+ * end-to-end. Anything else would intercept TLS and break fingerprinting.
374
+ */
375
+ export function assertTunnelingScheme(url) {
376
+ let scheme;
317
377
  try {
318
- const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey);
319
- options.telemetry?.recordProxyResolution(allocated.telemetry);
320
- const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, options.proxyAttempt);
378
+ scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
379
+ }
380
+ catch {
381
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
382
+ }
383
+ if (scheme !== "http" && scheme !== "socks5") {
384
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`);
385
+ }
386
+ }
387
+ export async function resolveWithVendor(vendor, policy, options, context) {
388
+ if (vendor === "nodemaven") {
389
+ const startedAt = Date.now();
390
+ const username = (context.credentials === undefined
391
+ ? process.env[NODEMAVEN_USERNAME_ENV]
392
+ : context.credentials[NODEMAVEN_USERNAME_ENV])?.trim();
393
+ const password = (context.credentials === undefined
394
+ ? process.env[NODEMAVEN_PASSWORD_ENV]
395
+ : context.credentials[NODEMAVEN_PASSWORD_ENV])?.trim();
396
+ const filter = context.credentials === undefined
397
+ ? process.env[NODEMAVEN_FILTER_ENV]
398
+ : context.credentials[NODEMAVEN_FILTER_ENV];
399
+ if (!username || !password) {
400
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`, { vendor: "nodemaven" });
401
+ }
402
+ const synthesized = synthesizeNodemavenProxy({
403
+ policy,
404
+ credentials: {
405
+ username,
406
+ password,
407
+ ...(filter ? { filter } : {}),
408
+ },
409
+ affinityKey: options.affinityKey,
410
+ protocol: context.protocol,
411
+ poolIndex: context.poolIndex,
412
+ refreshEpoch: context.refreshEpoch,
413
+ country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
414
+ });
415
+ options.telemetry?.recordProxyResolution({
416
+ provider: "nodemaven",
417
+ protocol: synthesized.protocol,
418
+ cacheStatus: "disabled",
419
+ cacheHit: false,
420
+ resolutionMs: Math.max(0, Date.now() - startedAt),
421
+ attempts: 1,
422
+ });
423
+ assertTunnelingScheme(synthesized.url);
321
424
  return {
322
425
  shouldWarn: false,
323
- url: allocated.pool.urls[poolIndex],
324
- source: "smartproxy-allocator",
426
+ url: synthesized.url,
427
+ source: "nodemaven-gateway",
428
+ protocol: synthesized.protocol,
325
429
  diagnostics: {
326
- ...allocated.pool.diagnostics,
327
- poolSize: allocated.pool.urls.length,
328
- poolIndex,
430
+ ...synthesized.diagnostics,
431
+ poolIndex: context.poolIndex,
329
432
  },
330
433
  };
331
434
  }
332
- catch (error) {
333
- if (error instanceof ProxyResolutionError && error.telemetry) {
334
- options.telemetry?.recordProxyResolution(error.telemetry);
335
- }
336
- if (policy.mode === "required") {
337
- throw error instanceof ProxyResolutionError
338
- ? error
339
- : new ProxyResolutionError("PROXY_ALLOCATION_FAILED", "Smartproxy allocator failed for required proxy egress.", { cause: error });
340
- }
341
- return { shouldWarn: true };
342
- }
435
+ // smartproxy allocation-style vendor.
436
+ const appKey = (context.credentials === undefined
437
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
438
+ : context.credentials[SMARTPROXY_APP_KEY_ENV])?.trim();
439
+ if (!appKey) {
440
+ // Guarded by vendorHasCredentials; treated as a vendor-internal failure.
441
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `${SMARTPROXY_APP_KEY_ENV} is not configured.`, { vendor: "smartproxy" });
442
+ }
443
+ const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
444
+ const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey, context.protocol, context.ambientDefaults !== false, context.sharedCache !== false);
445
+ options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
446
+ const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
447
+ const url = allocated.pool.urls[poolIndex];
448
+ if (url)
449
+ assertTunnelingScheme(url);
450
+ return {
451
+ shouldWarn: false,
452
+ url,
453
+ source: "smartproxy-allocator",
454
+ protocol: context.protocol,
455
+ diagnostics: {
456
+ ...allocated.pool.diagnostics,
457
+ poolSize: allocated.pool.urls.length,
458
+ poolIndex,
459
+ },
460
+ };
343
461
  }
344
462
  function resolvePolicy(options) {
345
463
  if (options.proxyPolicy) {
@@ -351,14 +469,179 @@ function resolvePolicy(options) {
351
469
  }
352
470
  return undefined;
353
471
  }
354
- function resolveProxyProvider(policy) {
355
- return (policy.provider ?? process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ?? "custom");
472
+ function isRegistryVendor(name) {
473
+ return name === "smartproxy" || name === "nodemaven";
474
+ }
475
+ function declaredVendorChain(policy) {
476
+ const declared = policy.providers?.length
477
+ ? policy.providers
478
+ : [policy.provider ?? envDefaultProvider()];
479
+ return declared.filter((vendor) => vendor !== undefined);
480
+ }
481
+ /**
482
+ * Ordered list of SDK-native proxy vendors declared by the policy. `providers`
483
+ * takes precedence over the legacy singular `provider`; the platform default
484
+ * env is the final fallback. Non-registry names (decodo/custom) are dropped so
485
+ * an all-deprecated chain has no managed adapter.
486
+ */
487
+ export function resolveVendorChain(policy) {
488
+ const chain = [];
489
+ for (const name of declaredVendorChain(policy)) {
490
+ if (isRegistryVendor(name) && !chain.includes(name)) {
491
+ chain.push(name);
492
+ }
493
+ }
494
+ return chain;
495
+ }
496
+ function envDefaultProvider() {
497
+ const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
498
+ return raw ?? undefined;
499
+ }
500
+ function vendorHasCredentials(vendor) {
501
+ if (vendor === "nodemaven")
502
+ return hasNodemavenCredentials();
503
+ return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
504
+ }
505
+ function missingCredentialEnv(vendor) {
506
+ return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
507
+ }
508
+ function vendorPoolSize(vendor, policy) {
509
+ return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
510
+ }
511
+ /**
512
+ * Total attempt span across a policy's vendor chain — the sum of each vendor's
513
+ * pool size. Transports use this so successive attempts rotate a vendor's pool
514
+ * and then fail over to the next vendor via the flat attempt index. With one
515
+ * vendor this equals that vendor's pool size (today's behaviour).
516
+ */
517
+ export function resolvePolicyProxyPoolSpan(policy) {
518
+ const chain = resolveVendorChain(policy);
519
+ if (chain.length === 0)
520
+ return resolveSmartproxyPoolSize(policy);
521
+ return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
522
+ }
523
+ function vendorMaxPoolSize(vendor) {
524
+ return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
525
+ }
526
+ /**
527
+ * Absolute upper bound on a chain's attempt span — the sum of each vendor's
528
+ * *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
529
+ * span), this backstop is independent of `session.poolSize`, so it never
530
+ * truncates a legitimately large pool below the point where the flat attempt
531
+ * index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
532
+ */
533
+ export function maxPolicyProxyPoolSpan(policy) {
534
+ const chain = resolveVendorChain(policy);
535
+ if (chain.length === 0)
536
+ return SMARTPROXY_MAX_POOL_SIZE;
537
+ return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
538
+ }
539
+ const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
540
+ /**
541
+ * Transport-retry attempt cap for a policy-managed request. A transport failure
542
+ * rotates the flat attempt index onto the *next* endpoint (and, once the index
543
+ * passes the primary vendor's pool span, the *next vendor*), so the cap must be
544
+ * the chain's full pool span for failover to reach the fallback vendor — the
545
+ * per-endpoint retry budget (default 3) never gets there.
546
+ *
547
+ * The span only widens beyond the caller's retry budget when ALL hold:
548
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
549
+ * - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
550
+ * is the documented total-attempt ceiling and must be honoured verbatim;
551
+ * - the method is safe/idempotent — an unsafe request must never be duplicated
552
+ * across the pool even if some framework default would allow it;
553
+ * - the policy resolves a non-empty *registry* vendor chain (smartproxy /
554
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed pool,
555
+ * so there is no possible endpoint crossover — they keep the retry budget.
556
+ *
557
+ * The widened cap is bounded by the chain's true maximum span (sum of each
558
+ * vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
559
+ * a pathological chain can never spin unbounded.
560
+ */
561
+ /**
562
+ * True when a policy request is in *implicit chain-rotation* mode: successive
563
+ * transport attempts rotate the flat index across the concatenated vendor pool
564
+ * spans (and, past the primary vendor's span, into the fallback vendor). This is
565
+ * the ONLY mode in which the transport loop widens its attempt cap AND
566
+ * de-duplicates repeated endpoints — the two behaviours must share one predicate
567
+ * so they never diverge. It holds when ALL of the widening conditions hold:
568
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
569
+ * - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
570
+ * the documented contract and must be honoured verbatim against whatever
571
+ * endpoint each attempt resolves (even a repeated one), so no de-duplication;
572
+ * - the method is safe/idempotent — an unsafe request is never duplicated;
573
+ * - the policy resolves a non-empty registry vendor chain (smartproxy /
574
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed
575
+ * endpoint, so there is nothing to rotate or de-duplicate.
576
+ */
577
+ export function policyRotatesTransportVendorChain(input) {
578
+ if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
579
+ return false;
580
+ }
581
+ if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
582
+ return false;
583
+ }
584
+ return resolveVendorChain(input.policy).length > 0;
585
+ }
586
+ export function resolvePolicyTransportAttemptCap(input) {
587
+ const budget = Math.max(1, Math.floor(input.retryAttempts));
588
+ if (!policyRotatesTransportVendorChain({
589
+ policy: input.policy,
590
+ usesPolicyAllocator: input.usesPolicyAllocator,
591
+ explicitRetry: input.explicitRetry,
592
+ method: input.method,
593
+ })) {
594
+ return budget;
595
+ }
596
+ const span = Math.min(maxPolicyProxyPoolSpan(input.policy), resolvePolicyProxyPoolSpan(input.policy));
597
+ return Math.max(budget, span);
598
+ }
599
+ /**
600
+ * A registry vendor chain (smartproxy/nodemaven) resolves a potentially
601
+ * *different* endpoint per flat attempt index, so a transport retry should
602
+ * advance across endpoints and de-duplicate once the chain stops yielding new
603
+ * ones. Deprecated custom/decodo policies have an empty registry chain and no
604
+ * managed endpoint, so the transport loop has nothing to rotate or de-duplicate.
605
+ */
606
+ export function policyResolvesRegistryVendorChain(policy) {
607
+ return Boolean(policy) && resolveVendorChain(policy).length > 0;
608
+ }
609
+ /** Map a resolved proxy source label to the vendor that served it. */
610
+ export function vendorFromResolvedSource(source) {
611
+ if (source === "nodemaven-gateway")
612
+ return "nodemaven";
613
+ if (source === "smartproxy-allocator")
614
+ return "smartproxy";
615
+ return undefined;
616
+ }
617
+ function normalizeAttemptIndex(attempt) {
618
+ return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
619
+ }
620
+ /**
621
+ * Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
622
+ * vendor's pool space in chain order. With a single vendor this reduces to
623
+ * `attempt % poolSize`, preserving today's behaviour exactly.
624
+ */
625
+ export function mapFlatAttempt(flat, sizes) {
626
+ let cursor = flat;
627
+ for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
628
+ const size = Math.max(1, sizes[vendorIndex] ?? 1);
629
+ if (cursor < size) {
630
+ return { vendorIndex, poolIndex: cursor };
631
+ }
632
+ cursor -= size;
633
+ }
634
+ return { vendorIndex: 0, poolIndex: 0 };
356
635
  }
357
- function resolveSmartproxyCountry(policy) {
358
- return (policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined);
636
+ function resolveSmartproxyCountry(policy, ambientDefaults = true) {
637
+ return (policy.geo?.country ??
638
+ (ambientDefaults
639
+ ? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
640
+ : undefined));
359
641
  }
360
- function resolveSmartproxyLifetime(policy) {
361
- const configuredLifetime = policy.session?.lifetimeMinutes ?? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
642
+ function resolveSmartproxyLifetime(policy, ambientDefaults = true) {
643
+ const configuredLifetime = policy.session?.lifetimeMinutes ??
644
+ (ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
362
645
  return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
363
646
  }
364
647
  function readPositiveNumberEnv(name, fallback) {
@@ -381,19 +664,24 @@ function selectProxyPoolIndex(poolSize, attempt = 0) {
381
664
  const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
382
665
  return normalizedAttempt % poolSize;
383
666
  }
384
- function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
667
+ function buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults = true) {
385
668
  const poolSize = resolveSmartproxyPoolSize(policy);
386
669
  return JSON.stringify({
387
670
  provider: "smartproxy",
388
- country: resolveSmartproxyCountry(policy),
671
+ credentialHash: createHash("sha256")
672
+ .update("apifuse-smartproxy-credential:v1\0")
673
+ .update(appKey)
674
+ .digest("hex"),
675
+ protocol,
676
+ country: resolveSmartproxyCountry(policy, ambientDefaults),
389
677
  affinity: policy.session?.affinity ?? "request",
390
678
  affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
391
679
  lifetimeMinutes,
392
680
  poolSize,
393
681
  });
394
682
  }
395
- async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey) {
396
- const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
683
+ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey, protocol, ambientDefaults = true, sharedCache = true) {
684
+ const cacheKey = buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults);
397
685
  const startedAt = Date.now();
398
686
  const now = startedAt;
399
687
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
@@ -401,7 +689,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
401
689
  const cached = proxyCache.get(cacheKey);
402
690
  if (!skipCached && cached && isFresh(cached, now)) {
403
691
  if (shouldSoftRefresh(cached, now)) {
404
- void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
692
+ void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
405
693
  return {
406
694
  pool: cached,
407
695
  telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
@@ -414,7 +702,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
414
702
  telemetry: telemetryForPool(cached, "memory_hit", startedAt),
415
703
  };
416
704
  }
417
- if (!skipCached) {
705
+ if (!skipCached && sharedCache) {
418
706
  const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
419
707
  if (redisResult)
420
708
  return redisResult;
@@ -429,7 +717,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
429
717
  }),
430
718
  };
431
719
  }
432
- const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt).finally(() => {
720
+ const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache).finally(() => {
433
721
  proxyInflight.delete(cacheKey);
434
722
  });
435
723
  proxyInflight.set(cacheKey, promise);
@@ -460,9 +748,9 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
460
748
  telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
461
749
  };
462
750
  }
463
- async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes) {
751
+ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache) {
464
752
  try {
465
- await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
753
+ await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), protocol, ambientDefaults, sharedCache, {
466
754
  background: true,
467
755
  });
468
756
  }
@@ -470,10 +758,10 @@ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes)
470
758
  // Soft refresh is opportunistic; current fresh pool remains usable.
471
759
  }
472
760
  }
473
- async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options = {}) {
474
- const redis = getProxyRedis();
761
+ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache, options = {}) {
762
+ const redis = sharedCache ? getProxyRedis() : undefined;
475
763
  if (!redis || !(await ensureRedisReady(redis))) {
476
- return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator" });
764
+ return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator", protocol, ambientDefaults });
477
765
  }
478
766
  const poolKey = smartproxyRedisPoolKey(cacheKey);
479
767
  const lockKey = smartproxyRedisLockKey(cacheKey);
@@ -487,6 +775,8 @@ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinute
487
775
  cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
488
776
  redis,
489
777
  poolKey,
778
+ protocol,
779
+ ambientDefaults,
490
780
  });
491
781
  }
492
782
  finally {
@@ -599,7 +889,7 @@ async function readSmartproxyAllocatorBodyWithDeadline(response, signal) {
599
889
  }
600
890
  async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options) {
601
891
  const poolSize = resolveSmartproxyPoolSize(policy);
602
- const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
892
+ const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, options.protocol, options.ambientDefaults);
603
893
  const allocatorStartedAt = Date.now();
604
894
  const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
605
895
  let allocation;
@@ -609,7 +899,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
609
899
  lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
610
900
  break;
611
901
  }
612
- const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt);
902
+ const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt, options.protocol);
613
903
  if (attemptResult.ok) {
614
904
  allocation = attemptResult;
615
905
  break;
@@ -657,7 +947,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
657
947
  expiresAt: allocatedAt + ttlMs,
658
948
  diagnostics: {
659
949
  provider: "smartproxy",
660
- country: resolveSmartproxyCountry(policy) ?? "default",
950
+ country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
661
951
  lifetimeMinutes,
662
952
  affinity: policy.session?.affinity ?? "request",
663
953
  rawConnect: true,
@@ -692,7 +982,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
692
982
  }),
693
983
  };
694
984
  }
695
- async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt) {
985
+ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt, protocol) {
696
986
  const { controller, dispose } = createDeadlineAbortController(deadlineAt);
697
987
  let response;
698
988
  try {
@@ -735,7 +1025,7 @@ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt
735
1025
  bodyClass: "http_error",
736
1026
  };
737
1027
  }
738
- const urls = parseSmartproxyAllocatorProxies(body);
1028
+ const urls = parseSmartproxyAllocatorProxies(body, protocol);
739
1029
  const bodyClass = classifySmartproxyAllocatorBody(body, urls);
740
1030
  if (urls.length === 0) {
741
1031
  return {
@@ -770,17 +1060,24 @@ function smartproxyAllocatorFailureMessage(failure) {
770
1060
  }
771
1061
  return "Smartproxy allocator response did not contain a usable proxy endpoint.";
772
1062
  }
773
- function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize) {
1063
+ // Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
1064
+ // best-effort mapping pending live vendor confirmation; http is the default and
1065
+ // the only value exercised in production today.
1066
+ const SMARTPROXY_PROTOCOL_PARAM = {
1067
+ http: "1",
1068
+ socks5: "2",
1069
+ };
1070
+ function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, protocol, ambientDefaults = true) {
774
1071
  const params = new URLSearchParams({
775
1072
  app_key: appKey,
776
1073
  pt: "9",
777
1074
  num: String(poolSize),
778
1075
  life: String(lifetimeMinutes),
779
- protocol: "1",
1076
+ protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
780
1077
  format: "txt",
781
1078
  lb: "\\n",
782
1079
  });
783
- const country = resolveSmartproxyCountry(policy);
1080
+ const country = resolveSmartproxyCountry(policy, ambientDefaults);
784
1081
  if (country) {
785
1082
  params.set("cc", country);
786
1083
  }
@@ -788,7 +1085,8 @@ function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize)
788
1085
  // old path 404s into the marketing site); the API lives on the api host.
789
1086
  return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
790
1087
  }
791
- function parseSmartproxyAllocatorProxies(body) {
1088
+ function parseSmartproxyAllocatorProxies(body, protocol) {
1089
+ const scheme = protocol === "socks5" ? "socks5" : "http";
792
1090
  const trimmed = body.trim();
793
1091
  if (!trimmed) {
794
1092
  return [];
@@ -807,7 +1105,7 @@ function parseSmartproxyAllocatorProxies(body) {
807
1105
  const port = "port" in item && (typeof item.port === "string" || typeof item.port === "number")
808
1106
  ? item.port
809
1107
  : "";
810
- return ip && port ? `http://${ip}:${port}` : null;
1108
+ return ip && port ? `${scheme}://${ip}:${port}` : null;
811
1109
  })
812
1110
  .filter((url) => url !== null);
813
1111
  }
@@ -819,7 +1117,7 @@ function parseSmartproxyAllocatorProxies(body) {
819
1117
  .split(/\r?\n/)
820
1118
  .map((item) => item.trim())
821
1119
  .filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
822
- .map((line) => `http://${line}`);
1120
+ .map((line) => `${scheme}://${line}`);
823
1121
  }
824
1122
  function classifySmartproxyAllocatorBody(body, urls) {
825
1123
  if (urls.length > 0) {
@@ -847,11 +1145,14 @@ function markSmartproxyCacheInvalidated(options = {}) {
847
1145
  if (!policy || policy.mode === "disabled") {
848
1146
  return undefined;
849
1147
  }
850
- if (resolveProxyProvider(policy) !== "smartproxy") {
1148
+ if (!resolveVendorChain(policy).includes("smartproxy")) {
851
1149
  return undefined;
852
1150
  }
853
1151
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
854
- const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
1152
+ const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
1153
+ if (!appKey)
1154
+ return undefined;
1155
+ const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
855
1156
  invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
856
1157
  proxyCache.delete(cacheKey);
857
1158
  proxyInflight.delete(cacheKey);
@@ -897,16 +1198,12 @@ export async function loadApiFuseConfig(dir = process.cwd()) {
897
1198
  const tsPath = path.resolve(dir, "apifuse.config.ts");
898
1199
  if (existsSync(tsPath)) {
899
1200
  const config = await importConfig(tsPath);
900
- const resolvedConfig = config ?? {};
901
- syncProxyEnv(resolvedConfig);
902
- return resolvedConfig;
1201
+ return config ?? {};
903
1202
  }
904
1203
  const jsPath = path.resolve(dir, "apifuse.config.js");
905
1204
  if (existsSync(jsPath)) {
906
1205
  const config = await importConfig(jsPath);
907
- const resolvedConfig = config ?? {};
908
- syncProxyEnv(resolvedConfig);
909
- return resolvedConfig;
1206
+ return config ?? {};
910
1207
  }
911
1208
  return {};
912
1209
  }