@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
@@ -0,0 +1,1157 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ resolveProxyConfigAsync,
5
+ type ProxyResolutionOptions,
6
+ type ProxyUserAgentSource,
7
+ } from "../config/loader.js";
8
+ import { ProviderError } from "../errors.js";
9
+ import { getStealthProfile } from "../stealth/profiles.js";
10
+ import type {
11
+ ChallengeSolution,
12
+ ProviderCache,
13
+ ProviderChallenge,
14
+ ProviderChallengeKind,
15
+ ProviderProxyMode,
16
+ ProviderResolverConfig,
17
+ ProviderResolverVendor,
18
+ ResolverContext,
19
+ } from "../types.js";
20
+ import {
21
+ resolverChallengeAllowsDirectCache,
22
+ resolverChallengeIsCacheable,
23
+ resolverChallengeIsIdentityScoped,
24
+ resolverChallengeIssuingIdentity,
25
+ } from "./resolver-vendors/bindings.js";
26
+ import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
27
+ import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
28
+ import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
29
+ import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
30
+ import {
31
+ RESOLVER_VENDOR_CAPABILITIES,
32
+ type ResolverIdentity,
33
+ type ResolverIssuingIdentity,
34
+ type ResolverVendorAdapter,
35
+ type ResolverVendorTransport,
36
+ ResolverVendorUnavailableError,
37
+ type ResolverVendorUnavailableReason,
38
+ resolveProviderResolverVendors,
39
+ resolverVendorSupports,
40
+ } from "./resolver-vendors/types.js";
41
+ import {
42
+ createUnsupportedResolverClient,
43
+ RESOLVER_INSTRUMENTATION_METADATA,
44
+ type ResolverSolveWithRecorder,
45
+ } from "./resolver-shared.js";
46
+ import {
47
+ APIFUSE__CDP_POOL__URL,
48
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
49
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
50
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
51
+ APIFUSE__RESOLVER__TIMEOUT_MS,
52
+ DEFAULT_RESOLVER_TIMEOUT_MS,
53
+ } from "./resolver-config.js";
54
+ import { DEFAULT_STEALTH_PROFILE } from "./stealth.js";
55
+ import type { TraceRecorder } from "./trace.js";
56
+
57
+ export {
58
+ createUnsupportedResolverClient,
59
+ RESOLVER_INSTRUMENTATION_METADATA,
60
+ } from "./resolver-shared.js";
61
+ export {
62
+ APIFUSE__CDP_POOL__URL,
63
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
64
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
65
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
66
+ APIFUSE__RESOLVER__TIMEOUT_MS,
67
+ DEFAULT_RESOLVER_TIMEOUT_MS,
68
+ } from "./resolver-config.js";
69
+ export {
70
+ DEFAULT_RESOLVER_VENDOR_PREFERENCE,
71
+ resolveProviderResolverVendors,
72
+ } from "./resolver-vendors/types.js";
73
+
74
+ type EnvLike = Record<string, string | undefined>;
75
+
76
+ type ResolvedResolverVendor =
77
+ | {
78
+ readonly vendor: Exclude<ProviderResolverVendor, "custom">;
79
+ readonly available: true;
80
+ readonly configuration: string | undefined;
81
+ }
82
+ | {
83
+ readonly vendor: ProviderResolverVendor;
84
+ readonly available: false;
85
+ readonly reason: ResolverVendorUnavailableReason;
86
+ };
87
+
88
+ type ResolverChainAttempt = {
89
+ readonly vendor: ProviderResolverVendor;
90
+ readonly reason: ResolverVendorUnavailableReason;
91
+ readonly missingFields?: readonly string[];
92
+ readonly cause?: {
93
+ readonly name: string;
94
+ readonly message: string;
95
+ };
96
+ readonly upstreamHost?: string;
97
+ readonly phase?: string;
98
+ readonly round?: number;
99
+ };
100
+
101
+ type ResolverChainClient = ResolverContext & {
102
+ solve(
103
+ challenge: ProviderChallenge,
104
+ signal?: AbortSignal,
105
+ traceRecorder?: TraceRecorder,
106
+ ): Promise<ChallengeSolution>;
107
+ };
108
+
109
+ export interface ResolverRuntimeOptions {
110
+ readonly allowedHosts?: readonly string[];
111
+ readonly cache?: ProviderCache;
112
+ /** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
113
+ readonly proxyIntent?: {
114
+ readonly mode: ProviderProxyMode;
115
+ readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
116
+ readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
117
+ readonly telemetry?: ProxyResolutionOptions["telemetry"];
118
+ readonly userAgent?: string;
119
+ };
120
+ /** Server-owned context/proxy scope used only for identity-bound cache entries. */
121
+ readonly identityScope?: string;
122
+ /** SDK-owned transport already bound to the resolved proxy lease and client profile. */
123
+ readonly transport?: ResolverVendorTransport;
124
+ /** Creates an SDK-owned transport bound to the declared profile and server-owned scope. */
125
+ readonly createTransport?: (input: {
126
+ readonly clientProfile?: string;
127
+ /** Server-owned proxy/context scope; the SDK never accepts a caller-built identity. */
128
+ readonly identityScope?: string;
129
+ }) => ResolverVendorTransport;
130
+ }
131
+
132
+ type CachedResolverSolution = {
133
+ readonly expiresAtMs: number;
134
+ readonly issuerDigest: string;
135
+ readonly solution: ChallengeSolution;
136
+ };
137
+
138
+ type ResolverCacheIndex = {
139
+ readonly entries: readonly {
140
+ readonly direct: true;
141
+ readonly expiresAtMs: number;
142
+ readonly issuerDigest: string;
143
+ }[];
144
+ };
145
+
146
+ export type ResolverSolutionSource = "cache" | "vendor";
147
+
148
+ type ResolverSolutionInvalidationOutcome =
149
+ | "cache_disabled"
150
+ | "entry_deleted"
151
+ | "index_entry_deleted"
152
+ | "not_cookie_solution"
153
+ | "solution_not_cached";
154
+
155
+ const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
156
+ const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
157
+ const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
158
+ const resolverCaches = new WeakMap<object, ProviderCache | null>();
159
+ const solutionIssuerDigests = new WeakMap<object, string>();
160
+ const resolverSolutionSources = new WeakMap<object, ResolverSolutionSource>();
161
+ const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
162
+ "abort",
163
+ "aborted",
164
+ "at",
165
+ "closed",
166
+ "connect",
167
+ "connection",
168
+ "dns",
169
+ "during",
170
+ "econnrefused",
171
+ "econnreset",
172
+ "error",
173
+ "etimedout",
174
+ "failed",
175
+ "failure",
176
+ "fetch",
177
+ "from",
178
+ "get",
179
+ "lookup",
180
+ "network",
181
+ "post",
182
+ "reading",
183
+ "refused",
184
+ "request",
185
+ "reset",
186
+ "response",
187
+ "socket",
188
+ "timed",
189
+ "timeout",
190
+ "tls",
191
+ "to",
192
+ "unavailable",
193
+ "upstream",
194
+ "while",
195
+ "writing",
196
+ ]);
197
+
198
+ export type ResolverInstrumentationMetadata = {
199
+ readonly target: ResolverContext;
200
+ readonly traceRecorder: TraceRecorder;
201
+ };
202
+
203
+ export type ResolverAdapterFactory = (
204
+ configuration: string | undefined,
205
+ timeoutMs: number,
206
+ allowedHosts: readonly string[],
207
+ ) => ResolverVendorAdapter;
208
+
209
+ const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAdapterFactory>> = {
210
+ "2captcha"(configuration, timeoutMs, allowedHosts) {
211
+ if (configuration === undefined) {
212
+ throw new Error("2captcha resolver adapter factory requires an API key");
213
+ }
214
+ return createTwoCaptchaResolverVendorAdapter({
215
+ allowedHosts,
216
+ apiKey: configuration,
217
+ timeoutMs,
218
+ });
219
+ },
220
+ capsolver(configuration, timeoutMs, allowedHosts) {
221
+ return createCapsolverResolverVendorAdapter({
222
+ allowedHosts,
223
+ apiKey: configuration,
224
+ timeoutMs,
225
+ });
226
+ },
227
+ browser(configuration, timeoutMs, allowedHosts) {
228
+ return createBrowserResolverVendorAdapter({
229
+ allowedHosts,
230
+ cdpUrl: configuration,
231
+ timeoutMs,
232
+ });
233
+ },
234
+ };
235
+
236
+ export const RESOLVER_ADAPTER_REGISTRY: Partial<
237
+ Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>
238
+ > = resolverAdapterRegistry;
239
+
240
+ export function swapResolverAdapterFactoryForTests(
241
+ vendor: ProviderResolverVendor,
242
+ factory: ResolverAdapterFactory | undefined,
243
+ ): () => void {
244
+ const original = resolverAdapterRegistry[vendor];
245
+ if (factory === undefined) delete resolverAdapterRegistry[vendor];
246
+ else resolverAdapterRegistry[vendor] = factory;
247
+ let restored = false;
248
+ return () => {
249
+ if (restored) return;
250
+ restored = true;
251
+ if (original === undefined) delete resolverAdapterRegistry[vendor];
252
+ else resolverAdapterRegistry[vendor] = original;
253
+ };
254
+ }
255
+
256
+ let resolveDefaultResolverUserAgent: () => string | undefined = () =>
257
+ getStealthProfile(DEFAULT_STEALTH_PROFILE).userAgent;
258
+
259
+ /** Internal test seam; deliberately not re-exported from the package root. */
260
+ export function swapResolverDefaultUserAgentForTests(
261
+ resolver: (() => string | undefined) | undefined,
262
+ ): () => void {
263
+ const original = resolveDefaultResolverUserAgent;
264
+ resolveDefaultResolverUserAgent =
265
+ resolver ?? (() => getStealthProfile(DEFAULT_STEALTH_PROFILE).userAgent);
266
+ let restored = false;
267
+ return () => {
268
+ if (restored) return;
269
+ restored = true;
270
+ resolveDefaultResolverUserAgent = original;
271
+ };
272
+ }
273
+
274
+ // This is the sole allowlist for declared vendors whose registry entry may be absent.
275
+ // Remove a vendor here when its adapter is registered.
276
+ const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS: ReadonlySet<ProviderResolverVendor> = new Set([
277
+ "capmonster",
278
+ "custom",
279
+ ]);
280
+
281
+ type ResolverChainEntry = {
282
+ readonly id: ProviderResolverVendor;
283
+ supports(kind: ProviderChallengeKind): boolean;
284
+ createAdapter(): ResolverVendorAdapter;
285
+ };
286
+
287
+ function normalizedEnvValue(env: EnvLike, key: string): string | undefined {
288
+ const value = env[key]?.trim();
289
+ return value ? value : undefined;
290
+ }
291
+
292
+ function readPositiveIntegerEnv(env: EnvLike, name: string): string | undefined {
293
+ const raw = env[name]?.trim();
294
+ if (!raw) return undefined;
295
+ if (!/^[1-9]\d*$/.test(raw)) {
296
+ throw new Error(`${name} must be a positive integer`);
297
+ }
298
+ return raw;
299
+ }
300
+
301
+ function assertDeclaredKind(
302
+ requestedKind: ProviderChallengeKind,
303
+ declaredKinds: readonly ProviderChallengeKind[],
304
+ ): void {
305
+ if (declaredKinds.includes(requestedKind)) return;
306
+
307
+ const declared = declaredKinds.length > 0 ? declaredKinds.join(", ") : "none";
308
+ throw new ProviderError(
309
+ `Resolver kind "${requestedKind}" is not declared; declared kinds: ${declared}`,
310
+ {
311
+ code: "RESOLVER_KIND_NOT_DECLARED",
312
+ fix: `Add "${requestedKind}" to the provider's resolver.kinds declaration.`,
313
+ },
314
+ );
315
+ }
316
+
317
+ function createUnavailableAdapter(
318
+ vendor: ProviderResolverVendor,
319
+ reason: ResolverVendorUnavailableReason,
320
+ ): ResolverVendorAdapter {
321
+ return {
322
+ id: vendor,
323
+ supports(kind) {
324
+ return resolverVendorSupports(vendor, kind);
325
+ },
326
+ async solve() {
327
+ throw new ResolverVendorUnavailableError(vendor, reason);
328
+ },
329
+ };
330
+ }
331
+
332
+ function createAdapter(
333
+ vendor: ResolvedResolverVendor,
334
+ timeoutMs: number,
335
+ allowedHosts: readonly string[],
336
+ adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
337
+ ): ResolverVendorAdapter {
338
+ const factory = adapterFactories[vendor.vendor];
339
+ if (!factory && !KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS.has(vendor.vendor)) {
340
+ throw new Error(
341
+ `Resolver adapter factory is missing for implemented vendor "${vendor.vendor}"`,
342
+ );
343
+ }
344
+ if (!vendor.available) {
345
+ return createUnavailableAdapter(vendor.vendor, vendor.reason);
346
+ }
347
+
348
+ if (factory) {
349
+ return factory(vendor.configuration, timeoutMs, allowedHosts);
350
+ }
351
+ return createUnavailableAdapter(vendor.vendor, "not_implemented");
352
+ }
353
+
354
+ function assertKnownResolverVendor(vendor: string): asserts vendor is ProviderResolverVendor {
355
+ if (Object.hasOwn(RESOLVER_VENDOR_CAPABILITIES, vendor)) return;
356
+ throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
357
+ }
358
+
359
+ function throwUnsupportedKind(kind: ProviderChallengeKind, usingDefaultVendors: boolean): never {
360
+ throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
361
+ code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
362
+ fix: usingDefaultVendors
363
+ ? `No SDK default resolver vendor supports "${kind}". Declare an explicit resolver.vendors override with a supporting vendor.`
364
+ : `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
365
+ });
366
+ }
367
+
368
+ function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
369
+ const hasMissingChallengeInput = attempts.some(
370
+ (attempt) => attempt.reason === "missing_challenge_input",
371
+ );
372
+ const summary = attempts
373
+ .map(({ vendor, reason, missingFields }) =>
374
+ missingFields === undefined || missingFields.length === 0
375
+ ? `${vendor}: ${reason}`
376
+ : `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`,
377
+ )
378
+ .join(", ");
379
+ throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
380
+ code: "RESOLVER_CHAIN_EXHAUSTED",
381
+ fix: hasMissingChallengeInput
382
+ ? "Capture the named challenge fields or configure another supporting resolver vendor."
383
+ : "Configure another supporting resolver vendor or restore an unavailable vendor.",
384
+ details: attempts,
385
+ });
386
+ }
387
+
388
+ function assertClientProfileTransportContract(
389
+ clientProfile: string | undefined,
390
+ transport: ResolverVendorTransport | undefined,
391
+ ): void {
392
+ if (clientProfile === undefined || transport === undefined) return;
393
+ throw new ProviderError(
394
+ `Resolver client profile "${clientProfile}" cannot be applied to a pre-bound transport`,
395
+ {
396
+ code: "RESOLVER_CLIENT_PROFILE_TRANSPORT_CONFLICT",
397
+ fix: "Remove the pre-bound transport and provide createTransport({ clientProfile, identityScope }) so the SDK can apply the provider-declared profile.",
398
+ },
399
+ );
400
+ }
401
+
402
+ function adapterRequiresTransport(
403
+ adapter: ResolverVendorAdapter,
404
+ kind: ProviderChallengeKind,
405
+ ): boolean {
406
+ return typeof adapter.requiresTransport === "function"
407
+ ? adapter.requiresTransport(kind)
408
+ : adapter.requiresTransport === true;
409
+ }
410
+
411
+ function sanitizeDiagnosticUrl(rawUrl: string): string {
412
+ try {
413
+ const parsed = new URL(rawUrl);
414
+ if (parsed.username || parsed.password) return "[REDACTED_PROXY_URL]";
415
+ return `${parsed.protocol}//${parsed.host}`;
416
+ } catch {
417
+ return "[REDACTED_URL]";
418
+ }
419
+ }
420
+
421
+ function sanitizeCauseMessage(message: string): string {
422
+ const withoutUrls = message.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi, sanitizeDiagnosticUrl);
423
+ const withoutAssignments = withoutUrls.replace(/(?:^|\s)[^\s=]+=[^\s]*/g, " [REDACTED]");
424
+ const safeTokens = withoutAssignments
425
+ .replaceAll("\r", " ")
426
+ .replaceAll("\n", " ")
427
+ .split(/\s+/)
428
+ .filter(Boolean)
429
+ .map((token) => {
430
+ if (
431
+ token === "[REDACTED]" ||
432
+ token === "[REDACTED_PROXY_URL]" ||
433
+ /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token)
434
+ )
435
+ return token;
436
+ const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
437
+ return word.length > 0 &&
438
+ word.length <= 32 &&
439
+ SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
440
+ ? token
441
+ : "[REDACTED]";
442
+ });
443
+ return safeTokens
444
+ .filter((token, index) => token !== "[REDACTED]" || safeTokens[index - 1] !== token)
445
+ .join(" ")
446
+ .slice(0, 512);
447
+ }
448
+
449
+ function restrictResolverTransport(
450
+ transport: ResolverVendorTransport,
451
+ allowedHosts: readonly string[],
452
+ ): ResolverVendorTransport {
453
+ return {
454
+ async fetch(url, init) {
455
+ // Empty declarations remain deny-by-default, matching the adapter-factory/browser path.
456
+ assertResolverHostAllowed(url, allowedHosts);
457
+ const response = await transport.fetch(url, { ...init, redirect: "manual" });
458
+ const hasLocationHeader = Object.keys(response.headers).some(
459
+ (name) => name.toLowerCase() === "location",
460
+ );
461
+ if (response.status >= 300 && response.status < 400 && hasLocationHeader) {
462
+ throw new ProviderError("Resolver transport refused a redirect response", {
463
+ code: "RESOLVER_HOST_NOT_ALLOWED",
464
+ fix: "Use a non-redirecting http or https URL on a host declared in allowedHosts.",
465
+ });
466
+ }
467
+ return response;
468
+ },
469
+ };
470
+ }
471
+
472
+ function safeCause(error: ResolverVendorUnavailableError): ResolverChainAttempt["cause"] {
473
+ if (error.cause === undefined) return undefined;
474
+ const cause = error.cause;
475
+ const rawName = cause instanceof Error ? cause.name : "Error";
476
+ return {
477
+ name: /^[a-z\d_.:-]{1,64}$/i.test(rawName) ? rawName : "Error",
478
+ message: sanitizeCauseMessage(cause instanceof Error ? cause.message : String(cause)),
479
+ };
480
+ }
481
+
482
+ function safeUpstreamHost(host: string | undefined): string | undefined {
483
+ if (host === undefined) return undefined;
484
+ const trimmed = host.trim();
485
+ if (/^[a-z\d.-]+$/i.test(trimmed)) return trimmed.toLowerCase();
486
+ try {
487
+ return new URL(trimmed).hostname.toLowerCase();
488
+ } catch {
489
+ return undefined;
490
+ }
491
+ }
492
+
493
+ function safePhase(phase: string | undefined): string | undefined {
494
+ return phase !== undefined && /^[a-z\d_.:-]{1,64}$/i.test(phase) ? phase : undefined;
495
+ }
496
+
497
+ function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChainAttempt {
498
+ const cause = safeCause(error);
499
+ const upstreamHost = safeUpstreamHost(error.upstreamHost);
500
+ const phase = safePhase(error.phase);
501
+ const round =
502
+ Number.isSafeInteger(error.round) && (error.round as number) > 0 ? error.round : undefined;
503
+ return {
504
+ vendor: error.vendor,
505
+ reason: error.reason,
506
+ ...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
507
+ ...(cause ? { cause } : {}),
508
+ ...(upstreamHost ? { upstreamHost } : {}),
509
+ ...(phase ? { phase } : {}),
510
+ ...(round !== undefined ? { round } : {}),
511
+ };
512
+ }
513
+
514
+ function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Record<string, unknown> {
515
+ const attempt = unavailableAttempt(error);
516
+ return {
517
+ unavailability_reason: error.reason,
518
+ missing_fields: error.missingFields,
519
+ cause_name: attempt.cause?.name,
520
+ cause_message: attempt.cause?.message,
521
+ upstream_host: attempt.upstreamHost,
522
+ transport_phase: attempt.phase,
523
+ transport_round: attempt.round,
524
+ };
525
+ }
526
+
527
+ function challengeOrigin(challenge: ProviderChallenge): string {
528
+ return new URL(challenge.pageUrl).origin;
529
+ }
530
+
531
+ function resolverIdentityDigest(identity: ResolverIssuingIdentity): string {
532
+ return createHash("sha256")
533
+ .update(
534
+ JSON.stringify({
535
+ proxyUrl: identity.proxyUrl ?? null,
536
+ userAgent: identity.userAgent,
537
+ }),
538
+ )
539
+ .digest("hex");
540
+ }
541
+
542
+ function resolverIdentityScopeDigest(identityScope: string): string {
543
+ return createHash("sha256").update(JSON.stringify({ identityScope })).digest("hex");
544
+ }
545
+
546
+ function resolverSolutionCacheKey(
547
+ cache: ProviderCache,
548
+ challenge: ProviderChallenge,
549
+ issuerDigest: string,
550
+ ): string {
551
+ return cache.key(RESOLVER_SOLUTION_CACHE_NAMESPACE, {
552
+ kind: challenge.kind,
553
+ origin: challengeOrigin(challenge),
554
+ issuerDigest,
555
+ });
556
+ }
557
+
558
+ function resolverSolutionIndexCacheKey(cache: ProviderCache, challenge: ProviderChallenge): string {
559
+ return cache.key(RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE, {
560
+ kind: challenge.kind,
561
+ origin: challengeOrigin(challenge),
562
+ });
563
+ }
564
+
565
+ function isCachedResolverSolution(value: unknown): value is CachedResolverSolution {
566
+ try {
567
+ if (value === null || typeof value !== "object") return false;
568
+ const candidate = value as Partial<CachedResolverSolution>;
569
+ if (
570
+ typeof candidate.expiresAtMs !== "number" ||
571
+ typeof candidate.issuerDigest !== "string" ||
572
+ candidate.solution === null ||
573
+ typeof candidate.solution !== "object" ||
574
+ candidate.solution.form !== "cookies" ||
575
+ typeof candidate.solution.userAgent !== "string"
576
+ ) {
577
+ return false;
578
+ }
579
+
580
+ const cookies: unknown = candidate.solution.cookies;
581
+ return (
582
+ cookies !== null &&
583
+ typeof cookies === "object" &&
584
+ !Array.isArray(cookies) &&
585
+ Object.values(cookies).every((cookie) => typeof cookie === "string")
586
+ );
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+
592
+ function isResolverCacheIndex(value: unknown): value is ResolverCacheIndex {
593
+ if (value === null || typeof value !== "object") return false;
594
+ const entries = (value as Partial<ResolverCacheIndex>).entries;
595
+ return (
596
+ Array.isArray(entries) &&
597
+ entries.every(
598
+ (entry) =>
599
+ entry !== null &&
600
+ typeof entry === "object" &&
601
+ entry.direct === true &&
602
+ typeof entry.expiresAtMs === "number" &&
603
+ typeof entry.issuerDigest === "string",
604
+ )
605
+ );
606
+ }
607
+
608
+ function solutionExpiryMs(solution: ChallengeSolution): number | undefined {
609
+ if (solution.form !== "cookies") return undefined;
610
+ const expires = solution.expires ?? solution.sdkEstimatedExpires;
611
+ if (typeof expires !== "number" || !Number.isFinite(expires)) return undefined;
612
+ return expires * 1_000;
613
+ }
614
+
615
+ function rememberSolutionIssuer(solution: ChallengeSolution, issuerDigest: string): void {
616
+ if (typeof solution === "object" && solution !== null) {
617
+ solutionIssuerDigests.set(solution, issuerDigest);
618
+ }
619
+ }
620
+
621
+ function rememberSolutionSource(solution: ChallengeSolution, source: ResolverSolutionSource): void {
622
+ if (typeof solution === "object" && solution !== null) {
623
+ resolverSolutionSources.set(solution, source);
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Identify whether this exact SDK-returned solution object came from the cache
629
+ * or a vendor solve. Returns undefined for copied or caller-created objects.
630
+ */
631
+ export function getResolverSolutionSource(
632
+ solution: ChallengeSolution,
633
+ ): ResolverSolutionSource | undefined {
634
+ return resolverSolutionSources.get(solution);
635
+ }
636
+
637
+ async function readCachedSolution(
638
+ cache: ProviderCache,
639
+ challenge: ProviderChallenge,
640
+ issuerDigest: string,
641
+ now: number,
642
+ ): Promise<ChallengeSolution | undefined> {
643
+ const cached = await cache.get(resolverSolutionCacheKey(cache, challenge, issuerDigest));
644
+ if (!cached || !isCachedResolverSolution(cached.value)) return undefined;
645
+ if (cached.value.issuerDigest !== issuerDigest || cached.value.expiresAtMs <= now)
646
+ return undefined;
647
+ if (cached.value.solution.form !== "cookies") return undefined;
648
+ const solution = {
649
+ ...cached.value.solution,
650
+ cookies: { ...cached.value.solution.cookies },
651
+ };
652
+ rememberSolutionIssuer(solution, issuerDigest);
653
+ rememberSolutionSource(solution, "cache");
654
+ return solution;
655
+ }
656
+
657
+ async function findCachedSolution(
658
+ cache: ProviderCache,
659
+ challenge: ProviderChallenge,
660
+ identity: ResolverIssuingIdentity | undefined,
661
+ identityScope: string | undefined,
662
+ ): Promise<ChallengeSolution | undefined> {
663
+ const now = Date.now();
664
+ if (identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)) {
665
+ return await readCachedSolution(
666
+ cache,
667
+ challenge,
668
+ resolverIdentityScopeDigest(identityScope),
669
+ now,
670
+ );
671
+ }
672
+ if (identity) {
673
+ const lookupIdentity = resolverChallengeIssuingIdentity(challenge, identity);
674
+ return await readCachedSolution(cache, challenge, resolverIdentityDigest(lookupIdentity), now);
675
+ }
676
+
677
+ const index = await cache.get(resolverSolutionIndexCacheKey(cache, challenge));
678
+ if (!index || !isResolverCacheIndex(index.value)) return undefined;
679
+ for (const entry of index.value.entries) {
680
+ if (entry.expiresAtMs <= now) continue;
681
+ const solution = await readCachedSolution(cache, challenge, entry.issuerDigest, now);
682
+ if (solution) return solution;
683
+ }
684
+ return undefined;
685
+ }
686
+
687
+ async function writeResolverCacheIndex(
688
+ cache: ProviderCache,
689
+ challenge: ProviderChallenge,
690
+ entries: ResolverCacheIndex["entries"],
691
+ now: number,
692
+ ): Promise<void> {
693
+ const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
694
+ const liveEntries = entries.filter((entry) => entry.expiresAtMs > now);
695
+ if (liveEntries.length === 0) {
696
+ await cache.delete(indexKey);
697
+ return;
698
+ }
699
+
700
+ const ttlMs = Math.max(
701
+ 1,
702
+ Math.floor(Math.max(...liveEntries.map((entry) => entry.expiresAtMs)) - now),
703
+ );
704
+ await cache.set(indexKey, { entries: liveEntries } satisfies ResolverCacheIndex, { ttlMs });
705
+ }
706
+
707
+ async function cacheResolverSolution(
708
+ cache: ProviderCache,
709
+ challenge: ProviderChallenge,
710
+ solution: ChallengeSolution,
711
+ identity: ResolverIssuingIdentity,
712
+ identityScope: string | undefined,
713
+ ): Promise<void> {
714
+ if (!resolverChallengeIsCacheable(challenge)) return;
715
+ const expiresAtMs = solutionExpiryMs(solution);
716
+ const now = Date.now();
717
+ if (expiresAtMs === undefined) return;
718
+ const ttlMs = Math.floor(expiresAtMs - now);
719
+ if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS) return;
720
+
721
+ const scopedDigest =
722
+ identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
723
+ ? resolverIdentityScopeDigest(identityScope)
724
+ : undefined;
725
+ if (
726
+ !resolverChallengeAllowsDirectCache(challenge) &&
727
+ scopedDigest === undefined &&
728
+ identity.proxyUrl === undefined
729
+ ) {
730
+ return;
731
+ }
732
+ const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
733
+ await cache.set(
734
+ resolverSolutionCacheKey(cache, challenge, issuerDigest),
735
+ { expiresAtMs, issuerDigest, solution } satisfies CachedResolverSolution,
736
+ { ttlMs },
737
+ );
738
+ rememberSolutionIssuer(solution, issuerDigest);
739
+ if (scopedDigest !== undefined || identity.proxyUrl !== undefined) return;
740
+
741
+ const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
742
+ const current = await cache.get(indexKey);
743
+ const currentEntries = isResolverCacheIndex(current?.value) ? current.value.entries : [];
744
+ await writeResolverCacheIndex(
745
+ cache,
746
+ challenge,
747
+ [
748
+ ...currentEntries.filter((entry) => entry.issuerDigest !== issuerDigest),
749
+ { direct: true, expiresAtMs, issuerDigest },
750
+ ],
751
+ now,
752
+ );
753
+ }
754
+
755
+ async function invalidateResolverSolutionWithOutcome(
756
+ resolver: ResolverContext,
757
+ challenge: ProviderChallenge,
758
+ solution: ChallengeSolution,
759
+ ): Promise<ResolverSolutionInvalidationOutcome> {
760
+ const metadata = (
761
+ resolver as ResolverContext & {
762
+ readonly [RESOLVER_INSTRUMENTATION_METADATA]?: ResolverInstrumentationMetadata;
763
+ }
764
+ )[RESOLVER_INSTRUMENTATION_METADATA];
765
+ const cacheOwner = metadata?.target ?? resolver;
766
+ const invalidate = async (): Promise<ResolverSolutionInvalidationOutcome> => {
767
+ if (solution.form !== "cookies") return "not_cookie_solution";
768
+ if (!resolverCaches.has(cacheOwner)) {
769
+ throw new Error("Resolver cache registration lookup failed during solution invalidation");
770
+ }
771
+ const cache = resolverCaches.get(cacheOwner);
772
+ if (cache === null || cache === undefined) return "cache_disabled";
773
+ const issuerDigest = solutionIssuerDigests.get(solution);
774
+ if (!issuerDigest) return "solution_not_cached";
775
+ await cache.delete(resolverSolutionCacheKey(cache, challenge, issuerDigest));
776
+
777
+ const index = await cache.get(resolverSolutionIndexCacheKey(cache, challenge));
778
+ if (!index) return "entry_deleted";
779
+ if (!isResolverCacheIndex(index.value)) {
780
+ throw new Error("Resolver solution cache index is malformed during invalidation");
781
+ }
782
+ await writeResolverCacheIndex(
783
+ cache,
784
+ challenge,
785
+ index.value.entries.filter((entry) => entry.issuerDigest !== issuerDigest),
786
+ Date.now(),
787
+ );
788
+ return "index_entry_deleted";
789
+ };
790
+
791
+ if (!metadata) {
792
+ return await invalidate();
793
+ }
794
+ return await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
795
+ attributes: { challenge_kind: challenge.kind },
796
+ onSuccess: (outcome) => ({ outcome }),
797
+ });
798
+ }
799
+
800
+ /** Remove the cached entry for the exact solution object returned by this resolver. */
801
+ export async function invalidateResolverSolution(
802
+ resolver: ResolverContext,
803
+ challenge: ProviderChallenge,
804
+ solution: ChallengeSolution,
805
+ ): Promise<void> {
806
+ await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
807
+ }
808
+
809
+ /**
810
+ * Remove a solution only when it was returned from the resolver cache. Providers
811
+ * should call this when an upstream serves the same challenge after a cached
812
+ * solution was applied, so the next `solve()` mints a fresh solution.
813
+ */
814
+ export async function invalidateCachedResolverSolution(
815
+ resolver: ResolverContext,
816
+ challenge: ProviderChallenge,
817
+ solution: ChallengeSolution,
818
+ ): Promise<boolean> {
819
+ if (getResolverSolutionSource(solution) !== "cache") return false;
820
+ const outcome = await invalidateResolverSolutionWithOutcome(resolver, challenge, solution);
821
+ return outcome === "entry_deleted" || outcome === "index_entry_deleted";
822
+ }
823
+
824
+ async function resolveResolverIdentity(
825
+ proxyIntent: NonNullable<ResolverRuntimeOptions["proxyIntent"]>,
826
+ ): Promise<{
827
+ readonly identity?: ResolverIdentity;
828
+ readonly unavailableReason?: ResolverVendorUnavailableReason;
829
+ readonly userAgentSource?: ProxyUserAgentSource;
830
+ }> {
831
+ const userAgentSource: ProxyUserAgentSource = proxyIntent.userAgent ? "declared" : "defaulted";
832
+ let proxyUrl: string | undefined;
833
+ try {
834
+ const resolved = await resolveProxyConfigAsync({
835
+ upstream: proxyIntent.upstream,
836
+ affinityKey: proxyIntent.affinityKey,
837
+ telemetry: proxyIntent.telemetry
838
+ ? {
839
+ ...proxyIntent.telemetry,
840
+ recordProxyResolution(event) {
841
+ proxyIntent.telemetry?.recordProxyResolution({
842
+ ...event,
843
+ userAgentSource,
844
+ });
845
+ },
846
+ }
847
+ : undefined,
848
+ });
849
+ proxyUrl = resolved.url;
850
+ if (!proxyUrl) return { unavailableReason: "missing_proxy_identity", userAgentSource };
851
+ } catch {
852
+ // Lease failures contain infrastructure detail that must not cross the resolver
853
+ // boundary. A required policy is classified by the existing fail-closed guard.
854
+ return { unavailableReason: "missing_proxy_identity", userAgentSource };
855
+ }
856
+
857
+ try {
858
+ const userAgent = proxyIntent.userAgent || resolveDefaultResolverUserAgent();
859
+ if (!userAgent) return { unavailableReason: "missing_client_profile", userAgentSource };
860
+ return {
861
+ identity: { proxyUrl, userAgent },
862
+ userAgentSource,
863
+ };
864
+ } catch {
865
+ return { unavailableReason: "missing_client_profile", userAgentSource };
866
+ }
867
+ }
868
+
869
+ function createResolverChainClient(options: {
870
+ readonly kinds: readonly ProviderChallengeKind[];
871
+ readonly entries: readonly ResolverChainEntry[];
872
+ readonly unavailableReason?: string;
873
+ readonly usingDefaultVendors?: boolean;
874
+ readonly cache?: ProviderCache;
875
+ readonly identity?: ResolverIdentity;
876
+ readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
877
+ readonly identityScope?: string;
878
+ readonly transport?: ResolverVendorTransport;
879
+ readonly createTransport?: ResolverRuntimeOptions["createTransport"];
880
+ readonly clientProfile?: string;
881
+ readonly allowedHosts?: readonly string[];
882
+ }): ResolverChainClient {
883
+ assertClientProfileTransportContract(options.clientProfile, options.transport);
884
+ const client: ResolverChainClient = {
885
+ async solve(
886
+ challenge: ProviderChallenge,
887
+ signal: AbortSignal = new AbortController().signal,
888
+ traceRecorder?: TraceRecorder,
889
+ ) {
890
+ assertDeclaredKind(challenge.kind, options.kinds);
891
+ if (options.unavailableReason) {
892
+ throw new ProviderError(options.unavailableReason, {
893
+ code: "RESOLVER_UNAVAILABLE",
894
+ fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
895
+ });
896
+ }
897
+
898
+ const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
899
+ if (supportingEntries.length === 0) {
900
+ throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
901
+ }
902
+ signal.throwIfAborted();
903
+ const identityResolution = options.proxyIntent
904
+ ? await resolveResolverIdentity(options.proxyIntent)
905
+ : { identity: options.identity };
906
+ const identity = identityResolution.identity;
907
+ signal.throwIfAborted();
908
+ // Resolve a required proxy lease before consulting the cache. Solutions minted
909
+ // under a previous release are shared and long-lived, but a portable cached token
910
+ // must not bypass the upstream admission policy when no lease can be resolved.
911
+ const requiredProxyIdentityMissing =
912
+ options.proxyIntent?.mode === "required" && identity === undefined;
913
+ if (requiredProxyIdentityMissing) {
914
+ throwExhausted(
915
+ supportingEntries.map((entry) => ({
916
+ vendor: entry.id,
917
+ reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
918
+ })),
919
+ );
920
+ }
921
+ if (options.cache && resolverChallengeIsCacheable(challenge)) {
922
+ const cached = await findCachedSolution(
923
+ options.cache,
924
+ challenge,
925
+ identity,
926
+ options.identityScope,
927
+ );
928
+ if (cached) return cached;
929
+ }
930
+ const attempts: ResolverChainAttempt[] = [];
931
+ for (const entry of supportingEntries) {
932
+ const adapter = entry.createAdapter();
933
+ try {
934
+ const solveAttempt = () => {
935
+ const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
936
+ const unrestrictedTransport =
937
+ options.transport ??
938
+ (requiresTransport
939
+ ? options.createTransport?.({
940
+ clientProfile: options.clientProfile,
941
+ identityScope: options.identityScope,
942
+ })
943
+ : undefined);
944
+ if (requiresTransport && unrestrictedTransport === undefined) {
945
+ throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
946
+ }
947
+ const transport = unrestrictedTransport
948
+ ? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
949
+ : undefined;
950
+ return adapter.solve(challenge, identity, signal, traceRecorder, transport);
951
+ };
952
+ const solution = traceRecorder
953
+ ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
954
+ attributes: {
955
+ vendor: adapter.id,
956
+ challenge_kind: challenge.kind,
957
+ client_profile: options.clientProfile,
958
+ resolver_identity_source: identityResolution.userAgentSource,
959
+ },
960
+ onError(error) {
961
+ return error instanceof ResolverVendorUnavailableError
962
+ ? unavailableSpanAttributes(error)
963
+ : undefined;
964
+ },
965
+ })
966
+ : await solveAttempt();
967
+ if (
968
+ options.cache &&
969
+ resolverChallengeIsCacheable(challenge) &&
970
+ solution.form === "cookies" &&
971
+ solutionExpiryMs(solution) !== undefined
972
+ ) {
973
+ const issuingIdentity =
974
+ adapter.getIssuingIdentity?.(solution, identity, challenge) ??
975
+ resolverChallengeIssuingIdentity(challenge, {
976
+ ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
977
+ userAgent: solution.userAgent,
978
+ });
979
+ if (issuingIdentity) {
980
+ await cacheResolverSolution(
981
+ options.cache,
982
+ challenge,
983
+ solution,
984
+ issuingIdentity,
985
+ options.identityScope,
986
+ );
987
+ }
988
+ }
989
+ rememberSolutionSource(solution, "vendor");
990
+ return solution;
991
+ } catch (error) {
992
+ signal.throwIfAborted();
993
+ if (!(error instanceof ResolverVendorUnavailableError)) throw error;
994
+ // Every vendor-unavailable result, including missing_challenge_input, falls
995
+ // through so another adapter can solve with a different input contract.
996
+ attempts.push(unavailableAttempt(error));
997
+ }
998
+ }
999
+
1000
+ throwExhausted(attempts);
1001
+ },
1002
+ };
1003
+ resolverCaches.set(client, options.cache ?? null);
1004
+ return client;
1005
+ }
1006
+
1007
+ export function createResolverClient(options: {
1008
+ readonly kinds: readonly ProviderChallengeKind[];
1009
+ readonly adapters: readonly ResolverVendorAdapter[];
1010
+ readonly unavailableReason?: string;
1011
+ readonly cache?: ProviderCache;
1012
+ readonly identity?: ResolverIdentity;
1013
+ readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
1014
+ readonly transport?: ResolverVendorTransport;
1015
+ readonly createTransport?: ResolverRuntimeOptions["createTransport"];
1016
+ readonly clientProfile?: string;
1017
+ readonly allowedHosts?: readonly string[];
1018
+ }): ResolverChainClient {
1019
+ return createResolverChainClient({
1020
+ kinds: options.kinds,
1021
+ entries: options.adapters.map((adapter) => ({
1022
+ id: adapter.id,
1023
+ supports: (kind) => adapter.supports(kind),
1024
+ createAdapter: () => adapter,
1025
+ })),
1026
+ unavailableReason: options.unavailableReason,
1027
+ cache: options.cache,
1028
+ identity: options.identity,
1029
+ proxyIntent: options.proxyIntent,
1030
+ transport: options.transport,
1031
+ createTransport: options.createTransport,
1032
+ clientProfile: options.clientProfile,
1033
+ allowedHosts: options.allowedHosts,
1034
+ });
1035
+ }
1036
+
1037
+ function resolveVendorAvailability(
1038
+ vendor: ProviderResolverVendor,
1039
+ env: EnvLike,
1040
+ ): ResolvedResolverVendor {
1041
+ if (vendor === "custom") {
1042
+ return {
1043
+ vendor,
1044
+ available: false,
1045
+ reason: "missing_transport",
1046
+ };
1047
+ }
1048
+ if (vendor === "browser") {
1049
+ return {
1050
+ vendor,
1051
+ available: true,
1052
+ configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
1053
+ };
1054
+ }
1055
+
1056
+ const envKey =
1057
+ vendor === "2captcha"
1058
+ ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
1059
+ : vendor === "capsolver"
1060
+ ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
1061
+ : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
1062
+
1063
+ const configuration = normalizedEnvValue(env, envKey);
1064
+ return configuration
1065
+ ? { vendor, available: true, configuration }
1066
+ : { vendor, available: false, reason: "missing_credentials" };
1067
+ }
1068
+
1069
+ export function bindResolverSignal(
1070
+ resolver: ResolverContext,
1071
+ defaultSignal: AbortSignal | undefined,
1072
+ ): ResolverContext {
1073
+ if (!defaultSignal) return resolver;
1074
+ const boundResolver: ResolverContext = {
1075
+ solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
1076
+ return (resolver as Partial<ResolverSolveWithRecorder> & ResolverContext).solve(
1077
+ challenge,
1078
+ signal,
1079
+ traceRecorder,
1080
+ );
1081
+ },
1082
+ };
1083
+ if (resolverCaches.has(resolver)) {
1084
+ resolverCaches.set(boundResolver, resolverCaches.get(resolver) ?? null);
1085
+ }
1086
+ return boundResolver;
1087
+ }
1088
+
1089
+ function createResolverClientFromEnvInternal(
1090
+ config: ProviderResolverConfig | undefined,
1091
+ env: EnvLike,
1092
+ options: ResolverRuntimeOptions,
1093
+ adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
1094
+ ): ResolverContext {
1095
+ if (!config) {
1096
+ return createUnsupportedResolverClient("Provider does not declare resolver capability");
1097
+ }
1098
+ assertClientProfileTransportContract(config.clientProfile, options.transport);
1099
+
1100
+ const vendors = resolveProviderResolverVendors(config);
1101
+ if (config.vendors !== undefined && config.vendors.length === 0) {
1102
+ return createResolverChainClient({
1103
+ kinds: config.kinds,
1104
+ entries: [],
1105
+ unavailableReason: "Provider resolver vendor chain is empty",
1106
+ });
1107
+ }
1108
+
1109
+ const timeoutValue = readPositiveIntegerEnv(env, APIFUSE__RESOLVER__TIMEOUT_MS);
1110
+ const timeoutMs = timeoutValue === undefined ? DEFAULT_RESOLVER_TIMEOUT_MS : Number(timeoutValue);
1111
+ const allowedHosts = [...(options.allowedHosts ?? [])];
1112
+
1113
+ return createResolverChainClient({
1114
+ kinds: config.kinds,
1115
+ entries: vendors.map((configuredVendor) => {
1116
+ assertKnownResolverVendor(configuredVendor);
1117
+ const vendor = configuredVendor;
1118
+ return {
1119
+ id: vendor,
1120
+ supports: (kind: ProviderChallengeKind) => resolverVendorSupports(vendor, kind),
1121
+ createAdapter: () =>
1122
+ createAdapter(
1123
+ resolveVendorAvailability(vendor, env),
1124
+ timeoutMs,
1125
+ allowedHosts,
1126
+ adapterFactories,
1127
+ ),
1128
+ };
1129
+ }),
1130
+ usingDefaultVendors: config.vendors === undefined,
1131
+ cache: options.cache,
1132
+ proxyIntent: options.proxyIntent,
1133
+ identityScope: options.identityScope,
1134
+ transport: options.transport,
1135
+ createTransport: options.createTransport,
1136
+ clientProfile: config.clientProfile,
1137
+ allowedHosts,
1138
+ });
1139
+ }
1140
+
1141
+ export function createResolverClientFromEnv(
1142
+ config: ProviderResolverConfig | undefined,
1143
+ env: EnvLike = process.env,
1144
+ options: ResolverRuntimeOptions = {},
1145
+ ): ResolverContext {
1146
+ return createResolverClientFromEnvInternal(config, env, options, RESOLVER_ADAPTER_REGISTRY);
1147
+ }
1148
+
1149
+ /** Internal test seam; deliberately not re-exported from the package root. */
1150
+ export function createResolverClientFromEnvForTests(
1151
+ config: ProviderResolverConfig | undefined,
1152
+ env: EnvLike,
1153
+ options: ResolverRuntimeOptions,
1154
+ adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
1155
+ ): ResolverContext {
1156
+ return createResolverClientFromEnvInternal(config, env, options, adapterFactories);
1157
+ }