@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50

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 (365) hide show
  1. package/AUTHORING.md +537 -33
  2. package/CHANGELOG.md +201 -1
  3. package/README.md +83 -22
  4. package/SUBMISSION.md +2 -2
  5. package/bin/apifuse-check.ts +165 -11
  6. package/bin/apifuse-dev.ts +57 -13
  7. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  8. package/bin/apifuse-migrate-shape.ts +202 -0
  9. package/bin/apifuse-pack-check.ts +22 -2
  10. package/bin/apifuse-pack-smoke.ts +57 -2
  11. package/bin/apifuse-pack-types.ts +357 -38
  12. package/bin/apifuse-perf.ts +14 -13
  13. package/bin/apifuse-record.ts +709 -72
  14. package/bin/apifuse-submit-check.ts +2344 -324
  15. package/bin/apifuse-sync-assets.ts +117 -0
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/dist/auth-turn/index.d.ts +3 -3
  18. package/dist/auth-turn/index.js +1 -1
  19. package/dist/auth.d.ts +14 -0
  20. package/dist/auth.js +67 -0
  21. package/dist/ceremonies/index.d.ts +16 -0
  22. package/dist/ceremonies/index.js +141 -36
  23. package/dist/cli/commands.d.ts +1 -1
  24. package/dist/cli/commands.js +27 -0
  25. package/dist/cli/create.d.ts +4 -1
  26. package/dist/cli/create.js +42 -37
  27. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  28. package/dist/cli/migrate-operation-declaration.js +1178 -0
  29. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  30. package/dist/cli/migrate-operation-shape.js +113 -0
  31. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  32. package/dist/cli/migrate-provider-shape.js +578 -0
  33. package/dist/cli/prompt-assets.d.ts +80 -0
  34. package/dist/cli/prompt-assets.js +743 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  36. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  37. package/dist/cli/templates/provider/README.md.tpl +5 -5
  38. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  39. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  40. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  41. package/dist/config/loader.d.ts +179 -16
  42. package/dist/config/loader.js +435 -127
  43. package/dist/contract-serialization.js +4 -8
  44. package/dist/contract-types.d.ts +12 -5
  45. package/dist/contract.js +24 -10
  46. package/dist/declaration-validation.d.ts +33 -0
  47. package/dist/declaration-validation.js +266 -0
  48. package/dist/define.d.ts +66 -29
  49. package/dist/define.js +778 -69
  50. package/dist/dev.d.ts +3 -0
  51. package/dist/dev.js +1 -1
  52. package/dist/engine.d.ts +78 -0
  53. package/dist/engine.js +133 -0
  54. package/dist/error-observability.d.ts +7 -0
  55. package/dist/error-observability.js +61 -0
  56. package/dist/error-resolution.d.ts +4 -0
  57. package/dist/error-resolution.js +122 -0
  58. package/dist/errors.d.ts +33 -0
  59. package/dist/errors.js +40 -0
  60. package/dist/fixture-sanitization.d.ts +28 -0
  61. package/dist/fixture-sanitization.js +227 -0
  62. package/dist/health-scenario.d.ts +1842 -0
  63. package/dist/health-scenario.js +624 -0
  64. package/dist/index.d.ts +21 -10
  65. package/dist/index.js +12 -7
  66. package/dist/lint.d.ts +23 -11
  67. package/dist/lint.js +572 -69
  68. package/dist/native-address.d.ts +43 -0
  69. package/dist/native-address.js +281 -0
  70. package/dist/native-egress-policy.d.ts +31 -0
  71. package/dist/native-egress-policy.js +288 -0
  72. package/dist/observability.d.ts +5 -2
  73. package/dist/observability.js +48 -1
  74. package/dist/provider.d.ts +10 -2
  75. package/dist/provider.js +4 -1
  76. package/dist/runtime/auth-flow.d.ts +5 -1
  77. package/dist/runtime/auth-flow.js +6 -0
  78. package/dist/runtime/browser.d.ts +1 -0
  79. package/dist/runtime/browser.js +492 -49
  80. package/dist/runtime/cache.d.ts +1 -0
  81. package/dist/runtime/cache.js +169 -15
  82. package/dist/runtime/choice-wordlist.d.ts +9 -0
  83. package/dist/runtime/choice-wordlist.js +138 -0
  84. package/dist/runtime/choice.d.ts +13 -1
  85. package/dist/runtime/choice.js +490 -102
  86. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  87. package/dist/runtime/chrome149-header-order.js +289 -0
  88. package/dist/runtime/env.js +12 -0
  89. package/dist/runtime/executor.d.ts +3 -2
  90. package/dist/runtime/executor.js +26 -25
  91. package/dist/runtime/http.d.ts +1 -0
  92. package/dist/runtime/http.js +515 -53
  93. package/dist/runtime/insights.js +2 -2
  94. package/dist/runtime/instrumentation.d.ts +2 -2
  95. package/dist/runtime/instrumentation.js +366 -8
  96. package/dist/runtime/native-network-errors.d.ts +33 -0
  97. package/dist/runtime/native-network-errors.js +69 -0
  98. package/dist/runtime/native-network.d.ts +96 -0
  99. package/dist/runtime/native-network.js +1232 -0
  100. package/dist/runtime/ocr.d.ts +29 -0
  101. package/dist/runtime/ocr.js +440 -0
  102. package/dist/runtime/otlp.d.ts +71 -2
  103. package/dist/runtime/otlp.js +397 -16
  104. package/dist/runtime/proxy-errors.js +6 -2
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  108. package/dist/runtime/proxy-telemetry.js +154 -47
  109. package/dist/runtime/redirects.d.ts +29 -0
  110. package/dist/runtime/redirects.js +36 -0
  111. package/dist/runtime/redis.d.ts +1 -1
  112. package/dist/runtime/redis.js +4 -2
  113. package/dist/runtime/request-options.d.ts +68 -1
  114. package/dist/runtime/request-options.js +548 -0
  115. package/dist/runtime/resolver-config.d.ts +6 -0
  116. package/dist/runtime/resolver-config.js +6 -0
  117. package/dist/runtime/resolver-public.d.ts +1 -0
  118. package/dist/runtime/resolver-public.js +1 -0
  119. package/dist/runtime/resolver-shared.d.ts +14 -0
  120. package/dist/runtime/resolver-shared.js +12 -0
  121. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  122. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  123. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  124. package/dist/runtime/resolver-vendors/browser.js +377 -0
  125. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  126. package/dist/runtime/resolver-vendors/capsolver.js +531 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  132. package/dist/runtime/resolver-vendors/types.js +96 -0
  133. package/dist/runtime/resolver.d.ts +72 -0
  134. package/dist/runtime/resolver.js +771 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +3 -0
  138. package/dist/runtime/state.js +277 -71
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +41 -5
  142. package/dist/runtime/stealth.js +997 -291
  143. package/dist/runtime/stt.js +1 -12
  144. package/dist/runtime/timeout.d.ts +5 -0
  145. package/dist/runtime/timeout.js +12 -0
  146. package/dist/runtime/trace-config.d.ts +12 -0
  147. package/dist/runtime/trace-config.js +62 -0
  148. package/dist/runtime/trace.d.ts +5 -0
  149. package/dist/runtime/trace.js +43 -10
  150. package/dist/serve.d.ts +1 -1
  151. package/dist/serve.js +1 -1
  152. package/dist/server/error-observability.d.ts +1 -0
  153. package/dist/server/error-observability.js +1 -0
  154. package/dist/server/index.d.ts +5 -2
  155. package/dist/server/index.js +2 -2
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test.d.ts +14 -3
  159. package/dist/server/self-test.js +129 -58
  160. package/dist/server/serve-implementation.d.ts +230 -0
  161. package/dist/server/serve-implementation.js +2265 -0
  162. package/dist/server/serve.d.ts +1 -70
  163. package/dist/server/serve.js +1 -1131
  164. package/dist/server/trace-output.d.ts +6 -0
  165. package/dist/server/trace-output.js +79 -0
  166. package/dist/server/types.d.ts +30 -5
  167. package/dist/server/types.js +13 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +9 -4
  207. package/dist/stealth/profiles.js +160 -211
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.js +7 -1
  211. package/dist/testing/index.d.ts +2 -1
  212. package/dist/testing/index.js +2 -1
  213. package/dist/testing/run.d.ts +32 -2
  214. package/dist/testing/run.js +489 -21
  215. package/dist/trace-sanitization.d.ts +5 -0
  216. package/dist/trace-sanitization.js +45 -0
  217. package/dist/types.d.ts +673 -131
  218. package/dist/types.js +1 -0
  219. package/package.json +45 -5
  220. package/src/auth-turn/index.ts +1 -1
  221. package/src/auth.ts +118 -0
  222. package/src/ceremonies/index.ts +189 -46
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  246. package/src/cli/commands.ts +33 -0
  247. package/src/cli/create.ts +50 -37
  248. package/src/cli/migrate-operation-declaration.ts +1654 -0
  249. package/src/cli/migrate-operation-shape.ts +184 -0
  250. package/src/cli/migrate-provider-shape.ts +772 -0
  251. package/src/cli/prompt-assets.ts +865 -0
  252. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  253. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  254. package/src/cli/templates/provider/README.md.tpl +5 -5
  255. package/src/cli/templates/provider/index.ts.tpl +6 -3
  256. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  257. package/src/cli/templates/provider/provider.json.tpl +6 -0
  258. package/src/config/loader.ts +690 -163
  259. package/src/contract-serialization.ts +5 -7
  260. package/src/contract-types.ts +12 -5
  261. package/src/contract.ts +24 -10
  262. package/src/declaration-validation.ts +330 -0
  263. package/src/define.ts +1048 -131
  264. package/src/dev.ts +4 -1
  265. package/src/engine.ts +279 -0
  266. package/src/error-observability.ts +64 -0
  267. package/src/error-resolution.ts +127 -0
  268. package/src/errors.ts +68 -0
  269. package/src/fixture-sanitization.ts +264 -0
  270. package/src/health-scenario.ts +875 -0
  271. package/src/index.ts +220 -13
  272. package/src/lint.ts +698 -90
  273. package/src/native-address.ts +340 -0
  274. package/src/native-egress-policy.ts +358 -0
  275. package/src/observability.ts +51 -1
  276. package/src/provider.ts +161 -3
  277. package/src/runtime/auth-flow.ts +12 -0
  278. package/src/runtime/browser.ts +661 -63
  279. package/src/runtime/cache.ts +189 -14
  280. package/src/runtime/choice-wordlist.ts +145 -0
  281. package/src/runtime/choice.ts +631 -120
  282. package/src/runtime/chrome149-header-order.ts +330 -0
  283. package/src/runtime/env.ts +13 -0
  284. package/src/runtime/executor.ts +43 -31
  285. package/src/runtime/http.ts +641 -61
  286. package/src/runtime/insights.ts +2 -2
  287. package/src/runtime/instrumentation.ts +520 -15
  288. package/src/runtime/native-network-errors.ts +99 -0
  289. package/src/runtime/native-network.ts +1605 -0
  290. package/src/runtime/ocr.ts +523 -0
  291. package/src/runtime/otlp.ts +467 -21
  292. package/src/runtime/proxy-errors.ts +12 -4
  293. package/src/runtime/proxy-nodemaven.ts +221 -0
  294. package/src/runtime/proxy-telemetry.ts +244 -75
  295. package/src/runtime/redirects.ts +66 -0
  296. package/src/runtime/redis.ts +7 -2
  297. package/src/runtime/request-options.ts +680 -1
  298. package/src/runtime/resolver-config.ts +6 -0
  299. package/src/runtime/resolver-public.ts +23 -0
  300. package/src/runtime/resolver-shared.ts +33 -0
  301. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  302. package/src/runtime/resolver-vendors/browser.ts +533 -0
  303. package/src/runtime/resolver-vendors/capsolver.ts +707 -0
  304. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  305. package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
  306. package/src/runtime/resolver-vendors/types.ts +212 -0
  307. package/src/runtime/resolver.ts +1157 -0
  308. package/src/runtime/secrets.ts +64 -0
  309. package/src/runtime/state.ts +394 -77
  310. package/src/runtime/stealth-cookies.ts +132 -0
  311. package/src/runtime/stealth.ts +1280 -336
  312. package/src/runtime/stt.ts +1 -19
  313. package/src/runtime/timeout.ts +18 -0
  314. package/src/runtime/trace-config.ts +78 -0
  315. package/src/runtime/trace.ts +57 -17
  316. package/src/serve.ts +6 -1
  317. package/src/server/error-observability.ts +1 -0
  318. package/src/server/index.ts +34 -2
  319. package/src/server/self-test-input-tokens.ts +29 -14
  320. package/src/server/self-test.ts +191 -68
  321. package/src/server/serve-implementation.ts +3378 -0
  322. package/src/server/serve.ts +1 -1632
  323. package/src/server/trace-output.ts +129 -0
  324. package/src/server/types.ts +13 -1
  325. package/src/stateful/README.md +146 -0
  326. package/src/stateful/errors.ts +35 -0
  327. package/src/stateful/http-provider-event-emitter.ts +314 -0
  328. package/src/stateful/http-session-owner-registry.ts +306 -0
  329. package/src/stateful/index.ts +18 -0
  330. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  331. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  332. package/src/stateful/provider-event-pipeline.ts +61 -0
  333. package/src/stateful/provider-events.ts +462 -0
  334. package/src/stateful/session-key.ts +111 -0
  335. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  336. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  337. package/src/stateful/stateful-provider-adapter.ts +562 -0
  338. package/src/stateful/stateful-provider-observability.ts +261 -0
  339. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  340. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  341. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  342. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  343. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  344. package/src/stateful-signing.ts +46 -0
  345. package/src/stealth/profiles.ts +202 -231
  346. package/src/stream-evidence.ts +988 -0
  347. package/src/stream.ts +8 -1
  348. package/src/testing/index.ts +10 -1
  349. package/src/testing/run.ts +658 -15
  350. package/src/trace-sanitization.ts +63 -0
  351. package/src/types.ts +788 -174
  352. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  353. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  354. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  355. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  356. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  357. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  358. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  359. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  360. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  361. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  362. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  363. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  364. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  365. /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, 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,212 @@ 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, options.engineCredentials)) {
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
+ credentials: options.engineCredentials,
317
+ });
318
+ }
319
+ catch (error) {
320
+ // Config/programming errors (invalid filter, etc.) are not vendor
321
+ // outages — propagate them rather than failing over.
322
+ if (!(error instanceof ProxyResolutionError)) {
323
+ throw error;
324
+ }
325
+ if (error.telemetry) {
326
+ options.telemetry?.recordProxyResolution(error.telemetry);
327
+ }
328
+ lastError = error;
329
+ options.telemetry?.recordProxyVendorFailover?.({
330
+ vendor,
331
+ nextVendor,
332
+ phase: "resolution",
333
+ reason: "allocation_failed",
334
+ });
313
335
  }
314
- return { shouldWarn: true };
315
336
  }
316
- const lifetimeMinutes = resolveSmartproxyLifetime(policy);
337
+ if (policy.mode === "required") {
338
+ if (lastError) {
339
+ throw lastError instanceof ProxyResolutionError
340
+ ? lastError
341
+ : new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`, { cause: lastError, vendorChain: chain });
342
+ }
343
+ if (blockedProtocol) {
344
+ 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 });
345
+ }
346
+ throw new ProxyResolutionError("PROXY_REQUIRED", `Proxy egress is required but no vendor credentials are configured. Missing: ${chain
347
+ .map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
348
+ .join(", ")}.`, { vendorChain: chain });
349
+ }
350
+ return { shouldWarn: true };
351
+ }
352
+ /**
353
+ * Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
354
+ * Vendor allocation and failover remain owned by the SDK.
355
+ */
356
+ export async function resolveProxy(options = {}) {
357
+ const resolved = await resolveProxyConfigAsync(options);
358
+ const vendor = vendorFromResolvedSource(resolved.source);
359
+ return vendor ? { ...resolved, vendor } : resolved;
360
+ }
361
+ /**
362
+ * Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
363
+ * CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
364
+ * for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
365
+ * Override per call via ProxyResolutionOptions.protocol (harness/tests).
366
+ */
367
+ export const VENDOR_DEFAULT_PROTOCOL = {
368
+ smartproxy: "http",
369
+ nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
370
+ };
371
+ /**
372
+ * Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
373
+ * (http CONNECT or socks5) so the client TLS handshake reaches the origin
374
+ * end-to-end. Anything else would intercept TLS and break fingerprinting.
375
+ */
376
+ export function assertTunnelingScheme(url) {
377
+ let scheme;
317
378
  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);
379
+ scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
380
+ }
381
+ catch {
382
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
383
+ }
384
+ if (scheme !== "http" && scheme !== "socks5") {
385
+ 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.`);
386
+ }
387
+ }
388
+ export async function resolveWithVendor(vendor, policy, options, context) {
389
+ if (vendor === "nodemaven") {
390
+ const startedAt = Date.now();
391
+ const username = (context.credentials === undefined
392
+ ? process.env[NODEMAVEN_USERNAME_ENV]
393
+ : context.credentials[NODEMAVEN_USERNAME_ENV])?.trim();
394
+ const password = (context.credentials === undefined
395
+ ? process.env[NODEMAVEN_PASSWORD_ENV]
396
+ : context.credentials[NODEMAVEN_PASSWORD_ENV])?.trim();
397
+ const filter = context.credentials === undefined
398
+ ? process.env[NODEMAVEN_FILTER_ENV]
399
+ : context.credentials[NODEMAVEN_FILTER_ENV];
400
+ if (!username || !password) {
401
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`, { vendor: "nodemaven" });
402
+ }
403
+ const synthesized = synthesizeNodemavenProxy({
404
+ policy,
405
+ credentials: {
406
+ username,
407
+ password,
408
+ ...(filter ? { filter } : {}),
409
+ },
410
+ affinityKey: options.affinityKey,
411
+ protocol: context.protocol,
412
+ poolIndex: context.poolIndex,
413
+ refreshEpoch: context.refreshEpoch,
414
+ country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
415
+ });
416
+ options.telemetry?.recordProxyResolution({
417
+ provider: "nodemaven",
418
+ protocol: synthesized.protocol,
419
+ cacheStatus: "disabled",
420
+ cacheHit: false,
421
+ resolutionMs: Math.max(0, Date.now() - startedAt),
422
+ attempts: 1,
423
+ });
424
+ assertTunnelingScheme(synthesized.url);
321
425
  return {
322
426
  shouldWarn: false,
323
- url: allocated.pool.urls[poolIndex],
324
- source: "smartproxy-allocator",
427
+ url: synthesized.url,
428
+ source: "nodemaven-gateway",
429
+ protocol: synthesized.protocol,
325
430
  diagnostics: {
326
- ...allocated.pool.diagnostics,
327
- poolSize: allocated.pool.urls.length,
328
- poolIndex,
431
+ ...synthesized.diagnostics,
432
+ poolIndex: context.poolIndex,
329
433
  },
330
434
  };
331
435
  }
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
- }
436
+ // smartproxy allocation-style vendor.
437
+ const appKey = (context.credentials === undefined
438
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
439
+ : context.credentials[SMARTPROXY_APP_KEY_ENV])?.trim();
440
+ if (!appKey) {
441
+ // Guarded by vendorHasCredentials; treated as a vendor-internal failure.
442
+ throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `${SMARTPROXY_APP_KEY_ENV} is not configured.`, { vendor: "smartproxy" });
443
+ }
444
+ const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
445
+ const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey, context.protocol, context.ambientDefaults !== false, context.sharedCache !== false);
446
+ options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
447
+ const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
448
+ const url = allocated.pool.urls[poolIndex];
449
+ if (url)
450
+ assertTunnelingScheme(url);
451
+ return {
452
+ shouldWarn: false,
453
+ url,
454
+ source: "smartproxy-allocator",
455
+ protocol: context.protocol,
456
+ diagnostics: {
457
+ ...allocated.pool.diagnostics,
458
+ poolSize: allocated.pool.urls.length,
459
+ poolIndex,
460
+ },
461
+ };
343
462
  }
344
463
  function resolvePolicy(options) {
345
464
  if (options.proxyPolicy) {
@@ -351,14 +470,187 @@ function resolvePolicy(options) {
351
470
  }
352
471
  return undefined;
353
472
  }
354
- function resolveProxyProvider(policy) {
355
- return (policy.provider ?? process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ?? "custom");
473
+ function isRegistryVendor(name) {
474
+ return name === "smartproxy" || name === "nodemaven";
475
+ }
476
+ function declaredVendorChain(policy) {
477
+ const declared = policy.providers?.length
478
+ ? policy.providers
479
+ : [policy.provider ?? envDefaultProvider()];
480
+ return declared.filter((vendor) => vendor !== undefined);
481
+ }
482
+ /**
483
+ * Ordered list of SDK-native proxy vendors declared by the policy. `providers`
484
+ * takes precedence over the legacy singular `provider`; the platform default
485
+ * env is the final fallback. Non-registry names (decodo/custom) are dropped so
486
+ * an all-deprecated chain has no managed adapter.
487
+ */
488
+ export function resolveVendorChain(policy) {
489
+ const chain = [];
490
+ for (const name of declaredVendorChain(policy)) {
491
+ if (isRegistryVendor(name) && !chain.includes(name)) {
492
+ chain.push(name);
493
+ }
494
+ }
495
+ return chain;
496
+ }
497
+ function envDefaultProvider() {
498
+ const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
499
+ return raw ?? undefined;
500
+ }
501
+ function vendorHasCredentials(vendor, credentials) {
502
+ if (vendor === "nodemaven") {
503
+ return Boolean((credentials === undefined
504
+ ? process.env[NODEMAVEN_USERNAME_ENV]
505
+ : credentials[NODEMAVEN_USERNAME_ENV])?.trim() &&
506
+ (credentials === undefined
507
+ ? process.env[NODEMAVEN_PASSWORD_ENV]
508
+ : credentials[NODEMAVEN_PASSWORD_ENV])?.trim());
509
+ }
510
+ return Boolean((credentials === undefined
511
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
512
+ : credentials[SMARTPROXY_APP_KEY_ENV])?.trim());
513
+ }
514
+ function missingCredentialEnv(vendor) {
515
+ return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
356
516
  }
357
- function resolveSmartproxyCountry(policy) {
358
- return (policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined);
517
+ function vendorPoolSize(vendor, policy) {
518
+ return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
359
519
  }
360
- function resolveSmartproxyLifetime(policy) {
361
- const configuredLifetime = policy.session?.lifetimeMinutes ?? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
520
+ /**
521
+ * Total attempt span across a policy's vendor chain — the sum of each vendor's
522
+ * pool size. Transports use this so successive attempts rotate a vendor's pool
523
+ * and then fail over to the next vendor via the flat attempt index. With one
524
+ * vendor this equals that vendor's pool size (today's behaviour).
525
+ */
526
+ export function resolvePolicyProxyPoolSpan(policy) {
527
+ const chain = resolveVendorChain(policy);
528
+ if (chain.length === 0)
529
+ return resolveSmartproxyPoolSize(policy);
530
+ return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
531
+ }
532
+ function vendorMaxPoolSize(vendor) {
533
+ return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
534
+ }
535
+ /**
536
+ * Absolute upper bound on a chain's attempt span — the sum of each vendor's
537
+ * *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
538
+ * span), this backstop is independent of `session.poolSize`, so it never
539
+ * truncates a legitimately large pool below the point where the flat attempt
540
+ * index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
541
+ */
542
+ export function maxPolicyProxyPoolSpan(policy) {
543
+ const chain = resolveVendorChain(policy);
544
+ if (chain.length === 0)
545
+ return SMARTPROXY_MAX_POOL_SIZE;
546
+ return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
547
+ }
548
+ const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
549
+ /**
550
+ * Transport-retry attempt cap for a policy-managed request. A transport failure
551
+ * rotates the flat attempt index onto the *next* endpoint (and, once the index
552
+ * passes the primary vendor's pool span, the *next vendor*), so the cap must be
553
+ * the chain's full pool span for failover to reach the fallback vendor — the
554
+ * per-endpoint retry budget (default 3) never gets there.
555
+ *
556
+ * The span only widens beyond the caller's retry budget when ALL hold:
557
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
558
+ * - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
559
+ * is the documented total-attempt ceiling and must be honoured verbatim;
560
+ * - the method is safe/idempotent — an unsafe request must never be duplicated
561
+ * across the pool even if some framework default would allow it;
562
+ * - the policy resolves a non-empty *registry* vendor chain (smartproxy /
563
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed pool,
564
+ * so there is no possible endpoint crossover — they keep the retry budget.
565
+ *
566
+ * The widened cap is bounded by the chain's true maximum span (sum of each
567
+ * vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
568
+ * a pathological chain can never spin unbounded.
569
+ */
570
+ /**
571
+ * True when a policy request is in *implicit chain-rotation* mode: successive
572
+ * transport attempts rotate the flat index across the concatenated vendor pool
573
+ * spans (and, past the primary vendor's span, into the fallback vendor). This is
574
+ * the ONLY mode in which the transport loop widens its attempt cap AND
575
+ * de-duplicates repeated endpoints — the two behaviours must share one predicate
576
+ * so they never diverge. It holds when ALL of the widening conditions hold:
577
+ * - the request is policy-allocator managed (not a caller-supplied proxy URL);
578
+ * - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
579
+ * the documented contract and must be honoured verbatim against whatever
580
+ * endpoint each attempt resolves (even a repeated one), so no de-duplication;
581
+ * - the method is safe/idempotent — an unsafe request is never duplicated;
582
+ * - the policy resolves a non-empty registry vendor chain (smartproxy /
583
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed
584
+ * endpoint, so there is nothing to rotate or de-duplicate.
585
+ */
586
+ export function policyRotatesTransportVendorChain(input) {
587
+ if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
588
+ return false;
589
+ }
590
+ if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
591
+ return false;
592
+ }
593
+ return resolveVendorChain(input.policy).length > 0;
594
+ }
595
+ export function resolvePolicyTransportAttemptCap(input) {
596
+ const budget = Math.max(1, Math.floor(input.retryAttempts));
597
+ if (!policyRotatesTransportVendorChain({
598
+ policy: input.policy,
599
+ usesPolicyAllocator: input.usesPolicyAllocator,
600
+ explicitRetry: input.explicitRetry,
601
+ method: input.method,
602
+ })) {
603
+ return budget;
604
+ }
605
+ const span = Math.min(maxPolicyProxyPoolSpan(input.policy), resolvePolicyProxyPoolSpan(input.policy));
606
+ return Math.max(budget, span);
607
+ }
608
+ /**
609
+ * A registry vendor chain (smartproxy/nodemaven) resolves a potentially
610
+ * *different* endpoint per flat attempt index, so a transport retry should
611
+ * advance across endpoints and de-duplicate once the chain stops yielding new
612
+ * ones. Deprecated custom/decodo policies have an empty registry chain and no
613
+ * managed endpoint, so the transport loop has nothing to rotate or de-duplicate.
614
+ */
615
+ export function policyResolvesRegistryVendorChain(policy) {
616
+ return Boolean(policy) && resolveVendorChain(policy).length > 0;
617
+ }
618
+ /** Map a resolved proxy source label to the vendor that served it. */
619
+ export function vendorFromResolvedSource(source) {
620
+ if (source === "nodemaven-gateway")
621
+ return "nodemaven";
622
+ if (source === "smartproxy-allocator")
623
+ return "smartproxy";
624
+ return undefined;
625
+ }
626
+ function normalizeAttemptIndex(attempt) {
627
+ return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
628
+ }
629
+ /**
630
+ * Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
631
+ * vendor's pool space in chain order. With a single vendor this reduces to
632
+ * `attempt % poolSize`, preserving today's behaviour exactly.
633
+ */
634
+ export function mapFlatAttempt(flat, sizes) {
635
+ let cursor = flat;
636
+ for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
637
+ const size = Math.max(1, sizes[vendorIndex] ?? 1);
638
+ if (cursor < size) {
639
+ return { vendorIndex, poolIndex: cursor };
640
+ }
641
+ cursor -= size;
642
+ }
643
+ return { vendorIndex: 0, poolIndex: 0 };
644
+ }
645
+ function resolveSmartproxyCountry(policy, ambientDefaults = true) {
646
+ return (policy.geo?.country ??
647
+ (ambientDefaults
648
+ ? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
649
+ : undefined));
650
+ }
651
+ function resolveSmartproxyLifetime(policy, ambientDefaults = true) {
652
+ const configuredLifetime = policy.session?.lifetimeMinutes ??
653
+ (ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
362
654
  return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
363
655
  }
364
656
  function readPositiveNumberEnv(name, fallback) {
@@ -381,19 +673,24 @@ function selectProxyPoolIndex(poolSize, attempt = 0) {
381
673
  const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
382
674
  return normalizedAttempt % poolSize;
383
675
  }
384
- function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
676
+ function buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults = true) {
385
677
  const poolSize = resolveSmartproxyPoolSize(policy);
386
678
  return JSON.stringify({
387
679
  provider: "smartproxy",
388
- country: resolveSmartproxyCountry(policy),
680
+ credentialHash: createHash("sha256")
681
+ .update("apifuse-smartproxy-credential:v1\0")
682
+ .update(appKey)
683
+ .digest("hex"),
684
+ protocol,
685
+ country: resolveSmartproxyCountry(policy, ambientDefaults),
389
686
  affinity: policy.session?.affinity ?? "request",
390
687
  affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
391
688
  lifetimeMinutes,
392
689
  poolSize,
393
690
  });
394
691
  }
395
- async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey) {
396
- const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
692
+ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey, protocol, ambientDefaults = true, sharedCache = true) {
693
+ const cacheKey = buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults);
397
694
  const startedAt = Date.now();
398
695
  const now = startedAt;
399
696
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
@@ -401,7 +698,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
401
698
  const cached = proxyCache.get(cacheKey);
402
699
  if (!skipCached && cached && isFresh(cached, now)) {
403
700
  if (shouldSoftRefresh(cached, now)) {
404
- void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
701
+ void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
405
702
  return {
406
703
  pool: cached,
407
704
  telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
@@ -414,7 +711,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
414
711
  telemetry: telemetryForPool(cached, "memory_hit", startedAt),
415
712
  };
416
713
  }
417
- if (!skipCached) {
714
+ if (!skipCached && sharedCache) {
418
715
  const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
419
716
  if (redisResult)
420
717
  return redisResult;
@@ -429,7 +726,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
429
726
  }),
430
727
  };
431
728
  }
432
- const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt).finally(() => {
729
+ const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache).finally(() => {
433
730
  proxyInflight.delete(cacheKey);
434
731
  });
435
732
  proxyInflight.set(cacheKey, promise);
@@ -460,9 +757,9 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
460
757
  telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
461
758
  };
462
759
  }
463
- async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes) {
760
+ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache) {
464
761
  try {
465
- await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
762
+ await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), protocol, ambientDefaults, sharedCache, {
466
763
  background: true,
467
764
  });
468
765
  }
@@ -470,10 +767,10 @@ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes)
470
767
  // Soft refresh is opportunistic; current fresh pool remains usable.
471
768
  }
472
769
  }
473
- async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options = {}) {
474
- const redis = getProxyRedis();
770
+ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache, options = {}) {
771
+ const redis = sharedCache ? getProxyRedis() : undefined;
475
772
  if (!redis || !(await ensureRedisReady(redis))) {
476
- return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator" });
773
+ return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator", protocol, ambientDefaults });
477
774
  }
478
775
  const poolKey = smartproxyRedisPoolKey(cacheKey);
479
776
  const lockKey = smartproxyRedisLockKey(cacheKey);
@@ -487,6 +784,8 @@ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinute
487
784
  cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
488
785
  redis,
489
786
  poolKey,
787
+ protocol,
788
+ ambientDefaults,
490
789
  });
491
790
  }
492
791
  finally {
@@ -599,7 +898,7 @@ async function readSmartproxyAllocatorBodyWithDeadline(response, signal) {
599
898
  }
600
899
  async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options) {
601
900
  const poolSize = resolveSmartproxyPoolSize(policy);
602
- const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
901
+ const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, options.protocol, options.ambientDefaults);
603
902
  const allocatorStartedAt = Date.now();
604
903
  const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
605
904
  let allocation;
@@ -609,7 +908,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
609
908
  lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
610
909
  break;
611
910
  }
612
- const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt);
911
+ const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt, options.protocol);
613
912
  if (attemptResult.ok) {
614
913
  allocation = attemptResult;
615
914
  break;
@@ -657,7 +956,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
657
956
  expiresAt: allocatedAt + ttlMs,
658
957
  diagnostics: {
659
958
  provider: "smartproxy",
660
- country: resolveSmartproxyCountry(policy) ?? "default",
959
+ country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
661
960
  lifetimeMinutes,
662
961
  affinity: policy.session?.affinity ?? "request",
663
962
  rawConnect: true,
@@ -692,7 +991,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
692
991
  }),
693
992
  };
694
993
  }
695
- async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt) {
994
+ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt, protocol) {
696
995
  const { controller, dispose } = createDeadlineAbortController(deadlineAt);
697
996
  let response;
698
997
  try {
@@ -735,7 +1034,7 @@ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt
735
1034
  bodyClass: "http_error",
736
1035
  };
737
1036
  }
738
- const urls = parseSmartproxyAllocatorProxies(body);
1037
+ const urls = parseSmartproxyAllocatorProxies(body, protocol);
739
1038
  const bodyClass = classifySmartproxyAllocatorBody(body, urls);
740
1039
  if (urls.length === 0) {
741
1040
  return {
@@ -770,17 +1069,24 @@ function smartproxyAllocatorFailureMessage(failure) {
770
1069
  }
771
1070
  return "Smartproxy allocator response did not contain a usable proxy endpoint.";
772
1071
  }
773
- function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize) {
1072
+ // Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
1073
+ // best-effort mapping pending live vendor confirmation; http is the default and
1074
+ // the only value exercised in production today.
1075
+ const SMARTPROXY_PROTOCOL_PARAM = {
1076
+ http: "1",
1077
+ socks5: "2",
1078
+ };
1079
+ function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, protocol, ambientDefaults = true) {
774
1080
  const params = new URLSearchParams({
775
1081
  app_key: appKey,
776
1082
  pt: "9",
777
1083
  num: String(poolSize),
778
1084
  life: String(lifetimeMinutes),
779
- protocol: "1",
1085
+ protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
780
1086
  format: "txt",
781
1087
  lb: "\\n",
782
1088
  });
783
- const country = resolveSmartproxyCountry(policy);
1089
+ const country = resolveSmartproxyCountry(policy, ambientDefaults);
784
1090
  if (country) {
785
1091
  params.set("cc", country);
786
1092
  }
@@ -788,7 +1094,8 @@ function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize)
788
1094
  // old path 404s into the marketing site); the API lives on the api host.
789
1095
  return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
790
1096
  }
791
- function parseSmartproxyAllocatorProxies(body) {
1097
+ function parseSmartproxyAllocatorProxies(body, protocol) {
1098
+ const scheme = protocol === "socks5" ? "socks5" : "http";
792
1099
  const trimmed = body.trim();
793
1100
  if (!trimmed) {
794
1101
  return [];
@@ -807,7 +1114,7 @@ function parseSmartproxyAllocatorProxies(body) {
807
1114
  const port = "port" in item && (typeof item.port === "string" || typeof item.port === "number")
808
1115
  ? item.port
809
1116
  : "";
810
- return ip && port ? `http://${ip}:${port}` : null;
1117
+ return ip && port ? `${scheme}://${ip}:${port}` : null;
811
1118
  })
812
1119
  .filter((url) => url !== null);
813
1120
  }
@@ -819,7 +1126,7 @@ function parseSmartproxyAllocatorProxies(body) {
819
1126
  .split(/\r?\n/)
820
1127
  .map((item) => item.trim())
821
1128
  .filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
822
- .map((line) => `http://${line}`);
1129
+ .map((line) => `${scheme}://${line}`);
823
1130
  }
824
1131
  function classifySmartproxyAllocatorBody(body, urls) {
825
1132
  if (urls.length > 0) {
@@ -847,11 +1154,16 @@ function markSmartproxyCacheInvalidated(options = {}) {
847
1154
  if (!policy || policy.mode === "disabled") {
848
1155
  return undefined;
849
1156
  }
850
- if (resolveProxyProvider(policy) !== "smartproxy") {
1157
+ if (!resolveVendorChain(policy).includes("smartproxy")) {
851
1158
  return undefined;
852
1159
  }
853
1160
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
854
- const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
1161
+ const appKey = (options.engineCredentials === undefined
1162
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
1163
+ : options.engineCredentials[SMARTPROXY_APP_KEY_ENV])?.trim();
1164
+ if (!appKey)
1165
+ return undefined;
1166
+ const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
855
1167
  invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
856
1168
  proxyCache.delete(cacheKey);
857
1169
  proxyInflight.delete(cacheKey);
@@ -897,16 +1209,12 @@ export async function loadApiFuseConfig(dir = process.cwd()) {
897
1209
  const tsPath = path.resolve(dir, "apifuse.config.ts");
898
1210
  if (existsSync(tsPath)) {
899
1211
  const config = await importConfig(tsPath);
900
- const resolvedConfig = config ?? {};
901
- syncProxyEnv(resolvedConfig);
902
- return resolvedConfig;
1212
+ return config ?? {};
903
1213
  }
904
1214
  const jsPath = path.resolve(dir, "apifuse.config.js");
905
1215
  if (existsSync(jsPath)) {
906
1216
  const config = await importConfig(jsPath);
907
- const resolvedConfig = config ?? {};
908
- syncProxyEnv(resolvedConfig);
909
- return resolvedConfig;
1217
+ return config ?? {};
910
1218
  }
911
1219
  return {};
912
1220
  }