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

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