@apifuse/provider-sdk 2.2.0-beta.23 → 2.2.0-beta.25

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 (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +2 -0
  3. package/bin/apifuse-pack-types.ts +234 -38
  4. package/bin/apifuse-perf.ts +15 -12
  5. package/bin/apifuse-record.ts +2 -0
  6. package/bin/apifuse-submit-check.ts +15 -2
  7. package/dist/config/loader.d.ts +8 -19
  8. package/dist/config/loader.js +28 -86
  9. package/dist/define.d.ts +4 -1
  10. package/dist/define.js +64 -6
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.js +2 -1
  13. package/dist/provider.d.ts +1 -1
  14. package/dist/runtime/auth-flow.js +2 -0
  15. package/dist/runtime/browser.js +50 -0
  16. package/dist/runtime/cache.d.ts +1 -0
  17. package/dist/runtime/cache.js +169 -15
  18. package/dist/runtime/http.js +0 -1
  19. package/dist/runtime/instrumentation.js +26 -1
  20. package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
  21. package/dist/runtime/resolver-vendors/bindings.js +15 -0
  22. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  23. package/dist/runtime/resolver-vendors/browser.js +287 -0
  24. package/dist/runtime/resolver-vendors/types.d.ts +42 -0
  25. package/dist/runtime/resolver-vendors/types.js +57 -0
  26. package/dist/runtime/resolver.d.ts +39 -0
  27. package/dist/runtime/resolver.js +414 -0
  28. package/dist/runtime/state.d.ts +3 -0
  29. package/dist/runtime/state.js +245 -141
  30. package/dist/runtime/stealth.js +3 -6
  31. package/dist/server/serve.d.ts +4 -1
  32. package/dist/server/serve.js +35 -8
  33. package/dist/testing/run.js +7 -0
  34. package/dist/types.d.ts +115 -7
  35. package/package.json +1 -1
  36. package/src/config/loader.ts +35 -111
  37. package/src/define.ts +105 -8
  38. package/src/index.ts +21 -1
  39. package/src/provider.ts +1 -0
  40. package/src/runtime/auth-flow.ts +2 -0
  41. package/src/runtime/browser.ts +69 -0
  42. package/src/runtime/cache.ts +189 -14
  43. package/src/runtime/http.ts +0 -1
  44. package/src/runtime/instrumentation.ts +36 -2
  45. package/src/runtime/resolver-vendors/bindings.ts +31 -0
  46. package/src/runtime/resolver-vendors/browser.ts +420 -0
  47. package/src/runtime/resolver-vendors/types.ts +113 -0
  48. package/src/runtime/resolver.ts +668 -0
  49. package/src/runtime/state.ts +323 -166
  50. package/src/runtime/stealth.ts +3 -6
  51. package/src/server/serve.ts +73 -5
  52. package/src/testing/run.ts +8 -0
  53. package/src/types.ts +133 -7
@@ -23,6 +23,7 @@ import {
23
23
  getTraceRecorder,
24
24
  type TraceContext,
25
25
  } from "./trace.js";
26
+ import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver.js";
26
27
 
27
28
  export interface InstrumentationOptions extends CreateTraceContextOptions {}
28
29
 
@@ -30,7 +31,7 @@ export type InstrumentedProviderContext<T extends ProviderContext> = Omit<T, "tr
30
31
  trace: TraceContext;
31
32
  };
32
33
 
33
- type InstrumentedNamespace = "http" | "stealth" | "browser" | "session" | "state";
34
+ type InstrumentedNamespace = "http" | "stealth" | "browser" | "session" | "state" | "resolver";
34
35
 
35
36
  const BROWSER_PAGE_METHODS = new Set(["goto", "fill", "click", "type", "waitForSelector"]);
36
37
  const DIAGNOSTIC_BASE_URL = "http://apifuse-instrumentation.invalid";
@@ -621,15 +622,47 @@ function wrapNamespace<T extends object>(
621
622
  }
622
623
 
623
624
  const wrappedMethods = new Map<PropertyKey, unknown>();
625
+ const resolverMetadata =
626
+ namespace === "resolver" ? { target, traceRecorder: recorder } : undefined;
624
627
 
625
628
  return new Proxy(target, {
626
629
  get(namespaceTarget, property, receiver) {
630
+ if (property === RESOLVER_INSTRUMENTATION_METADATA && resolverMetadata) {
631
+ return resolverMetadata;
632
+ }
627
633
  const value = Reflect.get(namespaceTarget, property, receiver);
628
634
 
629
635
  if (typeof value !== "function" || property === "constructor") {
630
636
  return value;
631
637
  }
632
638
 
639
+ if (namespace === "resolver" && property === "solve") {
640
+ if (wrappedMethods.has(property)) {
641
+ return wrappedMethods.get(property);
642
+ }
643
+
644
+ const wrapped = (...args: unknown[]) => {
645
+ const challenge = args[0];
646
+ const challengeKind =
647
+ typeof challenge === "object" &&
648
+ challenge !== null &&
649
+ "kind" in challenge &&
650
+ typeof challenge.kind === "string"
651
+ ? challenge.kind
652
+ : undefined;
653
+ return recorder.runSpan(
654
+ "resolver.solve",
655
+ () => Reflect.apply(value, namespaceTarget, [args[0], args[1], recorder]),
656
+ {
657
+ attributes: challengeKind ? { challenge_kind: challengeKind } : undefined,
658
+ },
659
+ );
660
+ };
661
+
662
+ wrappedMethods.set(property, wrapped);
663
+ return wrapped;
664
+ }
665
+
633
666
  if (namespace === "browser" && property === "newPage") {
634
667
  if (wrappedMethods.has(property)) {
635
668
  return wrappedMethods.get(property);
@@ -838,7 +871,8 @@ export function wrapWithInstrumentation<T extends ProviderContext>(
838
871
  property === "stealth" ||
839
872
  property === "browser" ||
840
873
  property === "session" ||
841
- property === "state"
874
+ property === "state" ||
875
+ property === "resolver"
842
876
  ) {
843
877
  const namespace = property;
844
878
  if (wrappedTargets.has(namespace)) {
@@ -0,0 +1,31 @@
1
+ import type { ProviderChallenge, ProviderChallengeKind } from "../../types.js";
2
+ import type { ResolverIssuingIdentity } from "./types.js";
3
+
4
+ export const RESOLVER_CHALLENGE_BINDINGS = {
5
+ aws_waf: "portable",
6
+ cloudflare_interstitial: "identity_scoped",
7
+ } as const satisfies Partial<
8
+ Readonly<Record<ProviderChallengeKind, "identity_scoped" | "portable">>
9
+ >;
10
+
11
+ export function resolverChallengeIsIdentityScoped(challenge: ProviderChallenge): boolean {
12
+ return (
13
+ RESOLVER_CHALLENGE_BINDINGS[challenge.kind as keyof typeof RESOLVER_CHALLENGE_BINDINGS] ===
14
+ "identity_scoped"
15
+ );
16
+ }
17
+
18
+ export function resolverChallengeIssuingIdentity(
19
+ challenge: ProviderChallenge,
20
+ identity: ResolverIssuingIdentity,
21
+ ): ResolverIssuingIdentity {
22
+ const binding = (
23
+ RESOLVER_CHALLENGE_BINDINGS as Partial<
24
+ Readonly<Record<ProviderChallengeKind, "identity_scoped" | "portable">>
25
+ >
26
+ )[challenge.kind];
27
+ if (binding === "portable") {
28
+ return { userAgent: identity.userAgent };
29
+ }
30
+ return identity;
31
+ }
@@ -0,0 +1,420 @@
1
+ import { isProviderError, ProviderError } from "../../errors.js";
2
+ import type {
3
+ BrowserClient,
4
+ BrowserCookie,
5
+ BrowserPage,
6
+ ChallengeSolution,
7
+ ProviderChallenge,
8
+ } from "../../types.js";
9
+ import { type BrowserClientOptions, createBrowserClient } from "../browser.js";
10
+ import type { TraceRecorder } from "../trace.js";
11
+ import { resolverChallengeIssuingIdentity } from "./bindings.js";
12
+ import {
13
+ type ResolverIdentity,
14
+ type ResolverVendorAdapter,
15
+ ResolverVendorUnavailableError,
16
+ } from "./types.js";
17
+
18
+ const BROWSER_VENDOR_ID = "browser" as const;
19
+ const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
20
+
21
+ const SUCCESS_COOKIE_NAMES = {
22
+ aws_waf: "aws-waf-token",
23
+ cloudflare_interstitial: "cf_clearance",
24
+ } as const;
25
+
26
+ type SupportedBrowserChallengeKind = keyof typeof SUCCESS_COOKIE_NAMES;
27
+
28
+ type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
29
+
30
+ export interface BrowserResolverVendorOptions {
31
+ readonly cdpUrl?: string;
32
+ readonly timeoutMs: number;
33
+ readonly pollIntervalMs?: number;
34
+ readonly allowedHosts: readonly string[];
35
+ readonly createClient?: BrowserClientFactory;
36
+ }
37
+
38
+ export type BrowserResolverSolution = Extract<ChallengeSolution, { readonly form: "cookies" }> & {
39
+ /** Unix seconds from the cookie that proved the challenge cleared. */
40
+ readonly expires?: number;
41
+ };
42
+
43
+ export interface BrowserResolverVendorAdapter extends ResolverVendorAdapter {
44
+ readonly id: "browser";
45
+ solve(
46
+ challenge: ProviderChallenge,
47
+ identity: ResolverIdentity | undefined,
48
+ signal: AbortSignal,
49
+ traceRecorder?: TraceRecorder,
50
+ ): Promise<BrowserResolverSolution>;
51
+ }
52
+
53
+ class BrowserSolveTimeoutError extends Error {
54
+ constructor() {
55
+ super("Browser resolver solve budget elapsed");
56
+ this.name = "BrowserSolveTimeoutError";
57
+ }
58
+ }
59
+
60
+ class BrowserCleanupTimeoutError extends Error {
61
+ constructor(timeoutMs: number) {
62
+ super(`Browser resolver cleanup exceeded ${timeoutMs}ms`);
63
+ this.name = "BrowserCleanupTimeoutError";
64
+ }
65
+ }
66
+
67
+ function abortReason(signal: AbortSignal): unknown {
68
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
69
+ }
70
+
71
+ function raceWithAbort<T>(operation: () => Promise<T>, signal: AbortSignal): Promise<T> {
72
+ if (signal.aborted) {
73
+ return Promise.reject(abortReason(signal));
74
+ }
75
+
76
+ return new Promise<T>((resolve, reject) => {
77
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
78
+ const onAbort = () => {
79
+ cleanup();
80
+ reject(abortReason(signal));
81
+ };
82
+ signal.addEventListener("abort", onAbort, { once: true });
83
+ operation().then(
84
+ (value) => {
85
+ cleanup();
86
+ resolve(value);
87
+ },
88
+ (error: unknown) => {
89
+ cleanup();
90
+ reject(error);
91
+ },
92
+ );
93
+ });
94
+ }
95
+
96
+ async function runBoundedCleanup(
97
+ cleanup: () => Promise<unknown>,
98
+ timeoutMs: number,
99
+ challengeKind: ProviderChallenge["kind"],
100
+ operation: "client.close" | "context.close",
101
+ traceRecorder: TraceRecorder | undefined,
102
+ ): Promise<void> {
103
+ let timeout: ReturnType<typeof setTimeout> | undefined;
104
+ const boundedCleanup = async () => {
105
+ try {
106
+ await Promise.race([
107
+ cleanup(),
108
+ new Promise<never>((_resolve, reject) => {
109
+ timeout = setTimeout(() => reject(new BrowserCleanupTimeoutError(timeoutMs)), timeoutMs);
110
+ }),
111
+ ]);
112
+ } finally {
113
+ if (timeout !== undefined) clearTimeout(timeout);
114
+ }
115
+ };
116
+
117
+ if (!traceRecorder) {
118
+ await boundedCleanup().catch(() => undefined);
119
+ return;
120
+ }
121
+
122
+ await traceRecorder
123
+ .runSpan("resolver.vendor.cleanup", boundedCleanup, {
124
+ attributes: {
125
+ vendor: BROWSER_VENDOR_ID,
126
+ challenge_kind: challengeKind,
127
+ operation,
128
+ },
129
+ onError(error) {
130
+ return {
131
+ error_message: error instanceof Error ? error.message : String(error),
132
+ ...(error instanceof Error && error.stack ? { error_stack: error.stack } : {}),
133
+ };
134
+ },
135
+ })
136
+ .catch(() => undefined);
137
+ }
138
+
139
+ async function abortableDelay(ms: number, signal: AbortSignal): Promise<void> {
140
+ let timer: ReturnType<typeof setTimeout> | undefined;
141
+ try {
142
+ await raceWithAbort(
143
+ () =>
144
+ new Promise<void>((resolve) => {
145
+ timer = setTimeout(resolve, ms);
146
+ }),
147
+ signal,
148
+ );
149
+ } finally {
150
+ if (timer !== undefined) clearTimeout(timer);
151
+ }
152
+ }
153
+
154
+ function isSupportedKind(kind: string): kind is SupportedBrowserChallengeKind {
155
+ return Object.hasOwn(SUCCESS_COOKIE_NAMES, kind);
156
+ }
157
+
158
+ function normalizedHostname(hostname: string): string {
159
+ return hostname.trim().toLowerCase().replace(/\.$/, "");
160
+ }
161
+
162
+ function assertChallengeHostAllowed(pageUrl: string, allowedHosts: readonly string[]): void {
163
+ const challengeHost = normalizedHostname(new URL(pageUrl).hostname);
164
+ const isAllowed = allowedHosts.some((host) => {
165
+ const declaredHost = normalizedHostname(host);
166
+ return declaredHost.length > 0 && !declaredHost.includes("*") && declaredHost === challengeHost;
167
+ });
168
+ if (isAllowed) return;
169
+
170
+ throw new ProviderError(`Resolver challenge host "${challengeHost}" is not declared`, {
171
+ code: "RESOLVER_HOST_NOT_ALLOWED",
172
+ fix: "Add the exact challenge hostname to the provider's allowedHosts declaration.",
173
+ });
174
+ }
175
+
176
+ function cookieDomainSpecificity(cookie: BrowserCookie): number {
177
+ return normalizedHostname(cookie.domain.replace(/^\./, "")).length;
178
+ }
179
+
180
+ function isHostOnlyCookieFor(cookie: BrowserCookie, hostname: string): boolean {
181
+ return (
182
+ !cookie.domain.startsWith(".") &&
183
+ normalizedHostname(cookie.domain) === normalizedHostname(hostname)
184
+ );
185
+ }
186
+
187
+ function cookieAppliesToUrl(cookie: BrowserCookie, url: URL): boolean {
188
+ const cookieDomain = normalizedHostname(cookie.domain.replace(/^\./, ""));
189
+ const requestHostname = normalizedHostname(url.hostname);
190
+ const domainMatches =
191
+ cookieDomain.length > 0 &&
192
+ (requestHostname === cookieDomain ||
193
+ (cookie.domain.startsWith(".") && requestHostname.endsWith(`.${cookieDomain}`)));
194
+ if (!domainMatches || (cookie.secure && url.protocol !== "https:")) return false;
195
+
196
+ const requestPath = url.pathname || "/";
197
+ const cookiePath = cookie.path;
198
+ return (
199
+ cookiePath.startsWith("/") &&
200
+ (requestPath === cookiePath ||
201
+ (requestPath.startsWith(cookiePath) &&
202
+ (cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/")))
203
+ );
204
+ }
205
+
206
+ function selectSuccessCookie(
207
+ cookies: readonly BrowserCookie[],
208
+ successCookieName: string,
209
+ pageUrl: string,
210
+ ): BrowserCookie | undefined {
211
+ const url = new URL(pageUrl);
212
+ return cookies
213
+ .filter((cookie) => cookie.name === successCookieName && cookieAppliesToUrl(cookie, url))
214
+ .sort(
215
+ (left, right) =>
216
+ Number(isHostOnlyCookieFor(right, url.hostname)) -
217
+ Number(isHostOnlyCookieFor(left, url.hostname)) ||
218
+ cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
219
+ right.path.length - left.path.length,
220
+ )[0];
221
+ }
222
+
223
+ async function solveInPage(
224
+ page: BrowserPage,
225
+ pageUrl: string,
226
+ successCookieName: string,
227
+ pollIntervalMs: number,
228
+ signal: AbortSignal,
229
+ ): Promise<BrowserResolverSolution> {
230
+ await raceWithAbort(() => page.goto(pageUrl), signal);
231
+
232
+ while (true) {
233
+ const cookies = await raceWithAbort(() => page.cookies(), signal);
234
+ const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
235
+ if (successCookie) {
236
+ const userAgent = await raceWithAbort(
237
+ () => page.evaluate<string>("navigator.userAgent"),
238
+ signal,
239
+ );
240
+ return {
241
+ form: "cookies",
242
+ cookies: { [successCookieName]: successCookie.value },
243
+ userAgent,
244
+ ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
245
+ };
246
+ }
247
+
248
+ await abortableDelay(pollIntervalMs, signal);
249
+ }
250
+ }
251
+
252
+ const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
253
+ -32_001, // queue full
254
+ -32_002, // acquire timed out
255
+ -32_003, // shutting down
256
+ ]);
257
+
258
+ function poolErrorCode(error: Error): number | undefined {
259
+ const code = (error as Error & { readonly code?: unknown }).code;
260
+ return typeof code === "number" ? code : undefined;
261
+ }
262
+
263
+ function knownUnavailableReason(
264
+ error: unknown,
265
+ ): "allocation_exhausted" | "missing_credentials" | "transport_failure" | undefined {
266
+ // Source-grounded mappings:
267
+ // - apps/cdp-pool/src/index.ts: the JSON-RPC codes and messages below.
268
+ // - src/runtime/browser.ts: BROWSER_CDP_POOL_REQUIRED and the two WebSocket messages.
269
+ // The pool's numeric JSON-RPC code is authoritative and is preferred whenever present.
270
+ // The message substrings remain as a fallback for pool builds predating code
271
+ // propagation; they are exact strings verified against the pool source. -32004 (unknown
272
+ // lease) and -32006 (missing allowedHosts) are deliberately unmapped: both are caller
273
+ // bugs, and the next vendor would fail identically, so they propagate unchanged.
274
+ if (isProviderError(error)) {
275
+ return error.code === "BROWSER_CDP_POOL_REQUIRED" ? "missing_credentials" : undefined;
276
+ }
277
+ if (!(error instanceof Error)) return undefined;
278
+
279
+ const code = poolErrorCode(error);
280
+ if (code !== undefined) {
281
+ return POOL_ALLOCATION_EXHAUSTED_CODES.has(code) ? "allocation_exhausted" : undefined;
282
+ }
283
+
284
+ if (
285
+ error.message.includes("CDP pool acquire queue is full") ||
286
+ error.message.includes("CDP pool acquire timed out") ||
287
+ error.message.includes("CDP pool is shutting down")
288
+ ) {
289
+ return "allocation_exhausted";
290
+ }
291
+
292
+ if (
293
+ error.message.includes("Unable to connect to WebSocket endpoint") ||
294
+ error.message.includes("WebSocket closed")
295
+ ) {
296
+ return "transport_failure";
297
+ }
298
+
299
+ return undefined;
300
+ }
301
+
302
+ async function closeBrowserClient(
303
+ client: BrowserClient | undefined,
304
+ timeoutMs: number,
305
+ challengeKind: ProviderChallenge["kind"],
306
+ traceRecorder: TraceRecorder | undefined,
307
+ ): Promise<void> {
308
+ const close = client?.close;
309
+ if (!close) return;
310
+ await runBoundedCleanup(
311
+ () => close.call(client),
312
+ timeoutMs,
313
+ challengeKind,
314
+ "client.close",
315
+ traceRecorder,
316
+ );
317
+ }
318
+
319
+ export function createBrowserResolverVendorAdapter(
320
+ options: BrowserResolverVendorOptions,
321
+ ): BrowserResolverVendorAdapter {
322
+ const createClient = options.createClient ?? createBrowserClient;
323
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
324
+
325
+ return {
326
+ id: BROWSER_VENDOR_ID,
327
+
328
+ supports(kind) {
329
+ return isSupportedKind(kind);
330
+ },
331
+
332
+ getIssuingIdentity(solution, requestedIdentity, challenge) {
333
+ if (solution.form !== "cookies" || !isSupportedKind(challenge.kind)) return undefined;
334
+ return resolverChallengeIssuingIdentity(challenge, {
335
+ ...(requestedIdentity ? { proxyUrl: requestedIdentity.proxyUrl } : {}),
336
+ userAgent: solution.userAgent,
337
+ });
338
+ },
339
+
340
+ async solve(challenge, identity, callerSignal, traceRecorder) {
341
+ void identity;
342
+ if (!options.cdpUrl?.trim()) {
343
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_credentials");
344
+ }
345
+ if (!isSupportedKind(challenge.kind)) {
346
+ throw new TypeError(`Browser resolver does not support ${challenge.kind}`);
347
+ }
348
+ assertChallengeHostAllowed(challenge.pageUrl, options.allowedHosts);
349
+ const challengeKind = challenge.kind;
350
+ callerSignal.throwIfAborted();
351
+
352
+ const solveController = new AbortController();
353
+ const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
354
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
355
+ const timeout = setTimeout(
356
+ () => solveController.abort(new BrowserSolveTimeoutError()),
357
+ options.timeoutMs,
358
+ );
359
+
360
+ let client: BrowserClient | undefined;
361
+ let handlerEntered = false;
362
+ try {
363
+ client = createClient({
364
+ allowedHosts: [...options.allowedHosts],
365
+ cdpUrl: options.cdpUrl.trim(),
366
+ requireCdpPool: true,
367
+ });
368
+ const contextOperation = client.withIsolatedContext(async (page) => {
369
+ handlerEntered = true;
370
+ return await solveInPage(
371
+ page,
372
+ challenge.pageUrl,
373
+ SUCCESS_COOKIE_NAMES[challengeKind],
374
+ pollIntervalMs,
375
+ solveController.signal,
376
+ );
377
+ });
378
+
379
+ try {
380
+ return await raceWithAbort(() => contextOperation, solveController.signal);
381
+ } catch (error) {
382
+ if (solveController.signal.aborted) {
383
+ if (handlerEntered) {
384
+ await runBoundedCleanup(
385
+ () => contextOperation,
386
+ options.timeoutMs,
387
+ challengeKind,
388
+ "context.close",
389
+ traceRecorder,
390
+ );
391
+ throw error;
392
+ }
393
+ await closeBrowserClient(client, options.timeoutMs, challengeKind, traceRecorder);
394
+ void contextOperation.catch(() => undefined);
395
+ }
396
+ throw error;
397
+ }
398
+ } catch (error) {
399
+ if (callerSignal.aborted) throw abortReason(callerSignal);
400
+ if (error instanceof BrowserSolveTimeoutError) {
401
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "timeout", {
402
+ cause: error,
403
+ });
404
+ }
405
+ if (error instanceof ResolverVendorUnavailableError) {
406
+ throw error;
407
+ }
408
+ const reason = knownUnavailableReason(error);
409
+ if (reason) {
410
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, reason, { cause: error });
411
+ }
412
+ throw error;
413
+ } finally {
414
+ clearTimeout(timeout);
415
+ callerSignal.removeEventListener("abort", onCallerAbort);
416
+ await closeBrowserClient(client, options.timeoutMs, challengeKind, traceRecorder);
417
+ }
418
+ },
419
+ };
420
+ }
@@ -0,0 +1,113 @@
1
+ import type {
2
+ ChallengeSolution,
3
+ ProviderChallenge,
4
+ ProviderChallengeKind,
5
+ ProviderResolverVendor,
6
+ } from "../../types.js";
7
+ import type { TraceRecorder } from "../trace.js";
8
+
9
+ export const RESOLVER_VENDOR_CAPABILITIES = {
10
+ browser: ["aws_waf", "cloudflare_interstitial"],
11
+ "2captcha": [
12
+ "turnstile",
13
+ "recaptcha_v2",
14
+ "recaptcha_v3",
15
+ "hcaptcha",
16
+ "cloudflare_interstitial",
17
+ "aws_waf",
18
+ ],
19
+ capsolver: [
20
+ "turnstile",
21
+ "recaptcha_v2",
22
+ "recaptcha_v3",
23
+ "hcaptcha",
24
+ "cloudflare_interstitial",
25
+ "aws_waf",
26
+ ],
27
+ capmonster: ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"],
28
+ custom: [
29
+ "turnstile",
30
+ "recaptcha_v2",
31
+ "recaptcha_v3",
32
+ "hcaptcha",
33
+ "cloudflare_interstitial",
34
+ "aws_waf",
35
+ ],
36
+ } as const satisfies Readonly<Record<ProviderResolverVendor, readonly ProviderChallengeKind[]>>;
37
+
38
+ export function resolverVendorSupports(
39
+ vendor: ProviderResolverVendor,
40
+ kind: ProviderChallengeKind,
41
+ ): boolean {
42
+ return (RESOLVER_VENDOR_CAPABILITIES[vendor] as readonly ProviderChallengeKind[]).includes(kind);
43
+ }
44
+
45
+ export interface ResolverIdentity {
46
+ readonly proxyUrl: string;
47
+ readonly userAgent: string;
48
+ }
49
+
50
+ export interface ResolverIssuingIdentity {
51
+ /** Absent when the adapter genuinely solved without a proxy. */
52
+ readonly proxyUrl?: string;
53
+ readonly userAgent: string;
54
+ }
55
+
56
+ export interface ResolverVendorAdapter {
57
+ readonly id: ProviderResolverVendor;
58
+ supports(kind: ProviderChallengeKind): boolean;
59
+ /** Identity the adapter actually used, reported after a successful solve. */
60
+ getIssuingIdentity?(
61
+ solution: ChallengeSolution,
62
+ requestedIdentity: ResolverIdentity | undefined,
63
+ challenge: ProviderChallenge,
64
+ ): ResolverIssuingIdentity | undefined;
65
+ solve(
66
+ challenge: ProviderChallenge,
67
+ identity: ResolverIdentity | undefined,
68
+ signal: AbortSignal,
69
+ traceRecorder?: TraceRecorder,
70
+ ): Promise<ChallengeSolution>;
71
+ }
72
+
73
+ export type ResolverVendorUnavailableReason =
74
+ | "missing_credentials"
75
+ | "missing_transport"
76
+ | "allocation_exhausted"
77
+ | "transport_failure"
78
+ | "timeout"
79
+ | "not_implemented";
80
+
81
+ export type ResolverChallengeVerdictReason = "human_puzzle";
82
+
83
+ type ResolverErrorOptions = {
84
+ readonly cause?: unknown;
85
+ };
86
+
87
+ export class ResolverVendorUnavailableError extends Error {
88
+ constructor(
89
+ readonly vendor: ProviderResolverVendor,
90
+ readonly reason: ResolverVendorUnavailableReason,
91
+ options: ResolverErrorOptions = {},
92
+ ) {
93
+ super(`Resolver vendor ${vendor} is unavailable: ${reason}`);
94
+ this.name = "ResolverVendorUnavailableError";
95
+ if (options.cause !== undefined) {
96
+ this.cause = options.cause;
97
+ }
98
+ }
99
+ }
100
+
101
+ export class ResolverChallengeVerdictError extends Error {
102
+ constructor(
103
+ readonly vendor: ProviderResolverVendor,
104
+ readonly reason: ResolverChallengeVerdictReason,
105
+ options: ResolverErrorOptions = {},
106
+ ) {
107
+ super(`Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
108
+ this.name = "ResolverChallengeVerdictError";
109
+ if (options.cause !== undefined) {
110
+ this.cause = options.cause;
111
+ }
112
+ }
113
+ }