@apifuse/provider-sdk 2.2.0-beta.32 → 2.2.0-beta.33

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.
@@ -3,7 +3,6 @@ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "..
3
3
  import { isProviderError, ProviderError } from "../errors.js";
4
4
  import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
5
5
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
6
- export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
7
6
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
8
7
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
9
8
  const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
@@ -16,7 +15,6 @@ export function createProviderChoiceContext(options) {
16
15
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
17
16
  const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
18
17
  if (resolvedStorage.mode === "server") {
19
- const issuance = resolveChoiceWordIssuance(options.env);
20
18
  const keys = hasRequestedChoiceBinding(issueOptions.bind)
21
19
  ? deriveManagedChoiceKeys({
22
20
  masterSecret: resolveMasterSecret(),
@@ -42,28 +40,8 @@ export function createProviderChoiceContext(options) {
42
40
  ttl_ms: issueOptions.ttlMs,
43
41
  binding,
44
42
  };
45
- if (issuance === "legacy") {
46
- const legacyKeys = keys ??
47
- deriveManagedChoiceKeys({
48
- masterSecret: resolveMasterSecret(),
49
- providerId: options.providerId,
50
- purpose: issueOptions.purpose,
51
- kid,
52
- });
53
- return issueLegacyServerStoredChoice({
54
- baseEnvelope,
55
- issueOptions,
56
- storage: resolvedStorage.storage,
57
- contextState: options.state,
58
- kid,
59
- keys: legacyKeys,
60
- issuedAtMs,
61
- });
62
- }
63
43
  return issueWordServerStoredChoice({
64
- baseEnvelope: {
65
- ...baseEnvelope,
66
- },
44
+ baseEnvelope,
67
45
  issueOptions,
68
46
  storage: resolvedStorage.storage,
69
47
  contextState: options.state,
@@ -307,20 +285,6 @@ function createLegacyExplicitParseResult(options) {
307
285
  },
308
286
  };
309
287
  }
310
- function resolveChoiceWordIssuance(env) {
311
- const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
312
- const value = configured?.trim() ?? "";
313
- if (value === "" || value === "legacy")
314
- return "legacy";
315
- if (value === "word")
316
- return "word";
317
- throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
318
- code: "CHOICE_WORD_ISSUANCE_INVALID",
319
- category: "input_validation",
320
- retryable: false,
321
- details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
322
- });
323
- }
324
288
  function resolveChoiceMasterSecret(options) {
325
289
  const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
326
290
  const trimmed = configured?.trim();
@@ -420,47 +384,6 @@ async function issueWordServerStoredChoice(options) {
420
384
  retryable: false,
421
385
  });
422
386
  }
423
- /**
424
- * Beta.28-compatible server handle issuance. The state stores the payload and
425
- * the client receives the existing six-part encrypted envelope whose payload
426
- * is a server-state handle.
427
- */
428
- async function issueLegacyServerStoredChoice(options) {
429
- const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
430
- const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
431
- if (payloadBytes > options.storage.maxValueBytes) {
432
- throw new ProviderError("Provider choice payload exceeds state storage policy.", {
433
- code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
434
- category: "input_validation",
435
- retryable: false,
436
- details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
437
- });
438
- }
439
- const stateId = `choice_${randomBytes(16).toString("base64url")}`;
440
- const namespace = resolveChoiceStateNamespace({
441
- storage: options.storage,
442
- contextState: options.contextState,
443
- ttlMs: options.issueOptions.ttlMs,
444
- });
445
- await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
446
- ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
447
- });
448
- const envelope = {
449
- ...options.baseEnvelope,
450
- payload: {
451
- storage: "server",
452
- state_id: stateId,
453
- payload_digest: digestChoicePayload(serializedPayload),
454
- created_at_ms: options.issuedAtMs,
455
- },
456
- };
457
- return encryptManagedChoiceToken({
458
- prefix: options.issueOptions.prefix,
459
- kid: options.kid,
460
- envelope,
461
- keys: options.keys,
462
- });
463
- }
464
387
  async function parseWordServerStoredChoice(options) {
465
388
  const storage = resolveParseStorage(options.parseOptions.storage);
466
389
  const namespace = resolveChoiceStateNamespace({
@@ -3,6 +3,8 @@ import { type BrowserClientOptions } from "../browser.js";
3
3
  import type { TraceRecorder } from "../trace.js";
4
4
  import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
5
5
  type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
6
+ /** Internal test seam; deliberately not re-exported from the package root. */
7
+ export declare function swapBrowserResolverClientFactoryForTests(factory: BrowserClientFactory | undefined): () => void;
6
8
  export interface BrowserResolverVendorOptions {
7
9
  readonly cdpUrl?: string;
8
10
  readonly timeoutMs: number;
@@ -5,10 +5,25 @@ import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.j
5
5
  import { ResolverVendorUnavailableError, } from "./types.js";
6
6
  const BROWSER_VENDOR_ID = "browser";
7
7
  const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
8
+ const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
9
+ const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
8
10
  const SUCCESS_COOKIE_NAMES = {
9
11
  aws_waf: "aws-waf-token",
10
12
  cloudflare_interstitial: "cf_clearance",
11
13
  };
14
+ let createResolverBrowserClient = createBrowserClient;
15
+ /** Internal test seam; deliberately not re-exported from the package root. */
16
+ export function swapBrowserResolverClientFactoryForTests(factory) {
17
+ const original = createResolverBrowserClient;
18
+ createResolverBrowserClient = factory ?? createBrowserClient;
19
+ let restored = false;
20
+ return () => {
21
+ if (restored)
22
+ return;
23
+ restored = true;
24
+ createResolverBrowserClient = original;
25
+ };
26
+ }
12
27
  class BrowserSolveTimeoutError extends Error {
13
28
  constructor() {
14
29
  super("Browser resolver solve budget elapsed");
@@ -126,22 +141,58 @@ function selectSuccessCookie(cookies, successCookieName, pageUrl) {
126
141
  cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
127
142
  right.path.length - left.path.length)[0];
128
143
  }
129
- async function solveInPage(page, pageUrl, successCookieName, pollIntervalMs, signal) {
130
- await raceWithAbort(() => page.goto(pageUrl), signal);
131
- while (true) {
132
- const cookies = await raceWithAbort(() => page.cookies(), signal);
133
- const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
134
- if (successCookie) {
135
- const userAgent = await raceWithAbort(() => page.evaluate("navigator.userAgent"), signal);
136
- return {
137
- form: "cookies",
138
- cookies: { [successCookieName]: successCookie.value },
139
- userAgent,
140
- ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
141
- };
144
+ async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, signal) {
145
+ return await page.withResourcePolicy({
146
+ allowedMethods: ["GET", "HEAD", "POST"],
147
+ documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
148
+ routes: [
149
+ {
150
+ match: () => true,
151
+ handle: (request) => ({
152
+ action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
153
+ ? "continue"
154
+ : "block",
155
+ }),
156
+ },
157
+ ],
158
+ }, async () => {
159
+ const userAgent = await raceWithAbort(() => page.evaluate("navigator.userAgent"), signal);
160
+ await raceWithAbort(() => page.goto(pageUrl), signal);
161
+ while (true) {
162
+ const cookies = await raceWithAbort(() => page.cookies(), signal);
163
+ const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
164
+ if (successCookie) {
165
+ return {
166
+ form: "cookies",
167
+ cookies: { [successCookieName]: successCookie.value },
168
+ userAgent,
169
+ ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
170
+ };
171
+ }
172
+ await abortableDelay(pollIntervalMs, signal);
142
173
  }
143
- await abortableDelay(pollIntervalMs, signal);
174
+ });
175
+ }
176
+ function isResolverBrowserRequestAllowed(targetUrl, challengeKind, allowedHosts) {
177
+ try {
178
+ assertResolverHostAllowed(targetUrl, allowedHosts);
179
+ return true;
180
+ }
181
+ catch {
182
+ if (challengeKind !== "aws_waf")
183
+ return false;
184
+ }
185
+ let target;
186
+ try {
187
+ target = new URL(targetUrl);
188
+ }
189
+ catch {
190
+ return false;
144
191
  }
192
+ const hostname = normalizedResolverHostname(target.hostname);
193
+ return (target.protocol === "https:" &&
194
+ hostname.length > AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX.length &&
195
+ hostname.endsWith(AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX));
145
196
  }
146
197
  const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
147
198
  -32_001, // queue full
@@ -188,7 +239,7 @@ async function closeBrowserClient(client, timeoutMs, challengeKind, traceRecorde
188
239
  await runBoundedCleanup(() => close.call(client), timeoutMs, challengeKind, "client.close", traceRecorder);
189
240
  }
190
241
  export function createBrowserResolverVendorAdapter(options) {
191
- const createClient = options.createClient ?? createBrowserClient;
242
+ const createClient = options.createClient ?? createResolverBrowserClient;
192
243
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
193
244
  return {
194
245
  id: BROWSER_VENDOR_ID,
@@ -235,10 +286,11 @@ export function createBrowserResolverVendorAdapter(options) {
235
286
  cdpUrl: cdpUrl ?? "",
236
287
  ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
237
288
  requireCdpPool: cdpUrl !== undefined,
289
+ serviceWorkers: "block",
238
290
  });
239
291
  const contextOperation = client.withIsolatedContext(async (page) => {
240
292
  handlerEntered = true;
241
- return await solveInPage(page, challenge.pageUrl, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
293
+ return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
242
294
  });
243
295
  try {
244
296
  return await raceWithAbort(() => contextOperation, solveController.signal);
@@ -16,9 +16,7 @@ export interface CapsolverResolverVendorOptions {
16
16
  }
17
17
  export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
18
18
  readonly id: "capsolver";
19
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
20
- readonly form: "token";
21
- }>>;
19
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
22
20
  }
23
21
  export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
24
22
  export {};
@@ -1,4 +1,6 @@
1
+ import { getStealthProfile } from "../../stealth/profiles.js";
1
2
  import { redactSensitiveText } from "../request-options.js";
3
+ import { DEFAULT_PROFILE } from "../stealth.js";
2
4
  import { assertResolverHostAllowed } from "./hosts.js";
3
5
  import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
4
6
  const CAPSOLVER_VENDOR_ID = "capsolver";
@@ -138,14 +140,45 @@ function parseCreateTaskResponse(payload) {
138
140
  }
139
141
  function parsePollResultResponse(payload) {
140
142
  const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
143
+ const cookies = solution && isJsonRecord(solution.cookies)
144
+ ? Object.fromEntries(Object.entries(solution.cookies).filter((entry) => typeof entry[1] === "string"))
145
+ : undefined;
141
146
  return {
142
147
  ...responseErrorFields(payload),
143
148
  status: typeof payload.status === "string" ? payload.status : undefined,
144
149
  solution: solution
145
- ? { token: typeof solution.token === "string" ? solution.token : undefined }
150
+ ? {
151
+ token: typeof solution.token === "string" ? solution.token : undefined,
152
+ cookie: typeof solution.cookie === "string" ? solution.cookie : undefined,
153
+ gRecaptchaResponse: typeof solution.gRecaptchaResponse === "string"
154
+ ? solution.gRecaptchaResponse
155
+ : undefined,
156
+ cookies,
157
+ userAgent: typeof solution.userAgent === "string" ? solution.userAgent : undefined,
158
+ }
146
159
  : undefined,
147
160
  };
148
161
  }
162
+ function proxyForCapsolver(proxyUrl) {
163
+ try {
164
+ const url = new URL(proxyUrl);
165
+ const protocol = url.protocol.slice(0, -1).toLowerCase();
166
+ const scheme = protocol === "http" || protocol === "socks5" ? protocol : undefined;
167
+ const port = Number(url.port || (scheme === "socks5" ? 1080 : 80));
168
+ if (!scheme || !url.hostname || !Number.isInteger(port) || port <= 0 || port > 65_535) {
169
+ return undefined;
170
+ }
171
+ const username = url.username ? decodeURIComponent(url.username) : "";
172
+ const password = url.password ? decodeURIComponent(url.password) : "";
173
+ if (username.includes(":") || password.includes(":"))
174
+ return undefined;
175
+ const value = `${scheme}:${url.hostname}:${port}${username || password ? `:${username}:${password}` : ""}`;
176
+ return { value, sensitive: [proxyUrl, value, username, password].filter(Boolean) };
177
+ }
178
+ catch {
179
+ return undefined;
180
+ }
181
+ }
149
182
  function isAllocationExhausted(payload) {
150
183
  const code = payload.errorCode?.toLowerCase() ?? "";
151
184
  const description = payload.errorDescription?.toLowerCase() ?? "";
@@ -266,7 +299,7 @@ export function createCapsolverResolverVendorAdapter(options) {
266
299
  supports(kind) {
267
300
  return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
268
301
  },
269
- async solve(challenge, _identity, callerSignal, traceRecorder) {
302
+ async solve(challenge, identity, callerSignal, traceRecorder) {
270
303
  const apiKey = options.apiKey?.trim();
271
304
  if (!apiKey) {
272
305
  throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
@@ -276,17 +309,34 @@ export function createCapsolverResolverVendorAdapter(options) {
276
309
  if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
277
310
  throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
278
311
  }
279
- if (challenge.kind !== "turnstile") {
280
- throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "not_implemented", {
312
+ const proxy = identity ? proxyForCapsolver(identity.proxyUrl) : undefined;
313
+ if (challenge.kind === "aws_waf" && identity && !proxy) {
314
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
315
+ phase: "create_task",
316
+ });
317
+ }
318
+ if (challenge.kind === "cloudflare_interstitial" && !identity) {
319
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_proxy_identity", {
281
320
  phase: "create_task",
282
321
  });
283
322
  }
323
+ if (identity && !proxy) {
324
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
325
+ phase: "create_task",
326
+ });
327
+ }
328
+ const challengeFields = challenge;
284
329
  const sensitiveValues = [
285
330
  apiKey,
286
331
  challenge.pageUrl,
287
- challenge.siteKey,
288
- ...(challenge.action !== undefined ? [challenge.action] : []),
289
- ...(challenge.cdata !== undefined ? [challenge.cdata] : []),
332
+ ...(typeof challengeFields.siteKey === "string" ? [challengeFields.siteKey] : []),
333
+ ...(typeof challengeFields.action === "string" ? [challengeFields.action] : []),
334
+ ...(typeof challengeFields.cdata === "string" ? [challengeFields.cdata] : []),
335
+ ...(typeof challengeFields.blockedHtml === "string" ? [challengeFields.blockedHtml] : []),
336
+ ...(typeof challengeFields.captchaScript === "string" ? [challengeFields.captchaScript] : []),
337
+ ...(typeof challengeFields.context === "string" ? [challengeFields.context] : []),
338
+ ...(typeof challengeFields.iv === "string" ? [challengeFields.iv] : []),
339
+ ...(proxy?.sensitive ?? []),
290
340
  ];
291
341
  assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
292
342
  callerSignal.throwIfAborted();
@@ -300,21 +350,69 @@ export function createCapsolverResolverVendorAdapter(options) {
300
350
  let phase = "create_task";
301
351
  try {
302
352
  const createTask = async () => {
303
- const metadata = {
304
- ...(challenge.action !== undefined ? { action: challenge.action } : {}),
305
- ...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
306
- };
307
- const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
308
- clientKey: apiKey,
309
- task: {
310
- type: "AntiTurnstileTaskProxyLess",
353
+ const task = challenge.kind === "aws_waf"
354
+ ? {
355
+ type: proxy ? "AntiAwsWafTask" : "AntiAwsWafTaskProxyLess",
311
356
  websiteURL: challenge.pageUrl,
312
- websiteKey: challenge.siteKey,
313
- ...(challenge.action !== undefined || challenge.cdata !== undefined
314
- ? { metadata }
357
+ ...(challenge.siteKey !== undefined ? { awsKey: challenge.siteKey } : {}),
358
+ ...(challenge.iv !== undefined ? { awsIv: challenge.iv } : {}),
359
+ ...(challenge.context !== undefined ? { awsContext: challenge.context } : {}),
360
+ ...(challenge.captchaScript !== undefined
361
+ ? { awsChallengeJS: challenge.captchaScript }
315
362
  : {}),
316
- },
317
- }, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
363
+ ...(proxy ? { proxy: proxy.value } : {}),
364
+ }
365
+ : challenge.kind === "turnstile"
366
+ ? {
367
+ type: "AntiTurnstileTaskProxyLess",
368
+ websiteURL: challenge.pageUrl,
369
+ websiteKey: challenge.siteKey,
370
+ ...(challenge.action !== undefined || challenge.cdata !== undefined
371
+ ? {
372
+ metadata: {
373
+ ...(challenge.action !== undefined ? { action: challenge.action } : {}),
374
+ ...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
375
+ },
376
+ }
377
+ : {}),
378
+ }
379
+ : challenge.kind === "recaptcha_v2"
380
+ ? {
381
+ type: proxy ? "ReCaptchaV2Task" : "ReCaptchaV2TaskProxyLess",
382
+ websiteURL: challenge.pageUrl,
383
+ websiteKey: challenge.siteKey,
384
+ ...(proxy ? { proxy: proxy.value } : {}),
385
+ }
386
+ : challenge.kind === "recaptcha_v3"
387
+ ? {
388
+ type: proxy ? "ReCaptchaV3Task" : "ReCaptchaV3TaskProxyLess",
389
+ websiteURL: challenge.pageUrl,
390
+ websiteKey: challenge.siteKey,
391
+ pageAction: challenge.action,
392
+ ...(challenge.minScore !== undefined ? { minScore: challenge.minScore } : {}),
393
+ ...(proxy ? { proxy: proxy.value } : {}),
394
+ }
395
+ : challenge.kind === "hcaptcha"
396
+ ? {
397
+ type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyLess",
398
+ websiteURL: challenge.pageUrl,
399
+ websiteKey: challenge.siteKey,
400
+ ...(proxy ? { proxy: proxy.value } : {}),
401
+ }
402
+ : challenge.kind === "cloudflare_interstitial"
403
+ ? {
404
+ type: "AntiCloudflareTask",
405
+ websiteURL: challenge.pageUrl,
406
+ proxy: proxy?.value,
407
+ ...(identity?.userAgent ? { userAgent: identity.userAgent } : {}),
408
+ ...(challenge.kind === "cloudflare_interstitial" && challenge.blockedHtml !== undefined
409
+ ? { html: challenge.blockedHtml }
410
+ : {}),
411
+ }
412
+ : (() => {
413
+ throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
414
+ })();
415
+ const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
318
416
  const taskId = createResult.payload.taskId;
319
417
  if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
320
418
  throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
@@ -352,15 +450,41 @@ export function createCapsolverResolverVendorAdapter(options) {
352
450
  case "ready":
353
451
  break;
354
452
  default:
355
- throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", { phase });
453
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
454
+ phase,
455
+ });
456
+ }
457
+ const solutionValue = challenge.kind === "aws_waf"
458
+ ? result.payload.solution?.cookie
459
+ : result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse;
460
+ if (challenge.kind === "cloudflare_interstitial") {
461
+ const cookies = result.payload.solution?.cookies;
462
+ const clearance = cookies?.cf_clearance ?? solutionValue;
463
+ if (!clearance?.trim()) {
464
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
465
+ phase,
466
+ });
467
+ }
468
+ return {
469
+ form: "cookies",
470
+ cookies: cookies && Object.keys(cookies).length > 0 ? cookies : { cf_clearance: clearance },
471
+ userAgent: result.payload.solution?.userAgent ??
472
+ identity?.userAgent ??
473
+ getStealthProfile(DEFAULT_PROFILE).userAgent,
474
+ };
356
475
  }
357
- const token = result.payload.solution?.token;
358
- if (!token?.trim()) {
476
+ if (!solutionValue?.trim()) {
359
477
  throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
360
478
  phase,
361
479
  });
362
480
  }
363
- return { form: "token", token };
481
+ return challenge.kind === "aws_waf"
482
+ ? {
483
+ form: "cookies",
484
+ cookies: { "aws-waf-token": solutionValue },
485
+ userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
486
+ }
487
+ : { form: "token", token: solutionValue };
364
488
  }
365
489
  };
366
490
  return traceRecorder
@@ -235,11 +235,6 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
235
235
  if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
236
236
  throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
237
237
  }
238
- if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
239
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
240
- phase: "create_task",
241
- });
242
- }
243
238
  if (challenge.kind === "aws_waf" &&
244
239
  (!challenge.siteKey?.trim() ||
245
240
  !challenge.captchaScript?.trim() ||
@@ -249,6 +244,11 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
249
244
  phase: "create_task",
250
245
  });
251
246
  }
247
+ if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
248
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
249
+ phase: "create_task",
250
+ });
251
+ }
252
252
  assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
253
253
  callerSignal.throwIfAborted();
254
254
  const proxy = identity ? parseProxyConfiguration(identity.proxyUrl) : undefined;
@@ -278,14 +278,47 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
278
278
  ...(identity ? { userAgent: identity.userAgent } : {}),
279
279
  ...(proxy ?? {}),
280
280
  }
281
- : {
282
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
283
- websiteURL: challenge.pageUrl,
284
- websiteKey: challenge.siteKey,
285
- isInvisible: false,
286
- ...(identity ? { userAgent: identity.userAgent } : {}),
287
- ...(proxy ?? {}),
288
- };
281
+ : challenge.kind === "recaptcha_v2"
282
+ ? {
283
+ type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
284
+ websiteURL: challenge.pageUrl,
285
+ websiteKey: challenge.siteKey,
286
+ isInvisible: false,
287
+ ...(identity ? { userAgent: identity.userAgent } : {}),
288
+ ...(proxy ?? {}),
289
+ }
290
+ : challenge.kind === "recaptcha_v3"
291
+ ? {
292
+ type: "RecaptchaV3TaskProxyless",
293
+ websiteURL: challenge.pageUrl,
294
+ websiteKey: challenge.siteKey,
295
+ minScore: challenge.minScore,
296
+ pageAction: challenge.action,
297
+ ...(identity ? { userAgent: identity.userAgent } : {}),
298
+ }
299
+ : challenge.kind === "hcaptcha"
300
+ ? {
301
+ type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
302
+ websiteURL: challenge.pageUrl,
303
+ websiteKey: challenge.siteKey,
304
+ ...(identity ? { userAgent: identity.userAgent } : {}),
305
+ ...(proxy ?? {}),
306
+ }
307
+ : challenge.kind === "turnstile"
308
+ ? {
309
+ type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
310
+ websiteURL: challenge.pageUrl,
311
+ websiteKey: challenge.siteKey,
312
+ ...(challenge.action !== undefined ? { action: challenge.action } : {}),
313
+ ...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
314
+ ...(identity ? { userAgent: identity.userAgent } : {}),
315
+ ...(proxy ?? {}),
316
+ }
317
+ : // `resolverVendorSupports` above already rejected every kind this
318
+ // adapter does not build a task for, so this branch is unreachable.
319
+ (() => {
320
+ throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
321
+ })();
289
322
  const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
290
323
  const taskId = taskIdFrom(createResult.payload);
291
324
  if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
@@ -2,7 +2,7 @@ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, Provi
2
2
  import type { TraceRecorder } from "../trace.js";
3
3
  export declare const RESOLVER_VENDOR_CAPABILITIES: {
4
4
  readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
5
- readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
5
+ readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "aws_waf"];
6
6
  readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
7
7
  readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
8
8
  readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
@@ -1,14 +1,17 @@
1
1
  export const RESOLVER_VENDOR_CAPABILITIES = {
2
2
  browser: ["aws_waf", "cloudflare_interstitial"],
3
+ // Every kind listed per vendor is implemented by that vendor's adapter; the
4
+ // per-adapter "agrees with every declared capability" tests iterate this
5
+ // table, so adding a kind here without an implementation fails the suite.
6
+ // 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
7
+ // `akamai_sensor`: their API offers no task type for them, so declaring them
8
+ // would route challenges to a vendor that can only refuse.
3
9
  "2captcha": [
4
10
  "turnstile",
5
11
  "recaptcha_v2",
6
12
  "recaptcha_v3",
7
13
  "hcaptcha",
8
- "cloudflare_interstitial",
9
14
  "aws_waf",
10
- "akamai_sec_cpt",
11
- "akamai_sensor",
12
15
  ],
13
16
  capsolver: [
14
17
  "turnstile",
@@ -34,7 +34,7 @@ export type ResolverInstrumentationMetadata = {
34
34
  readonly target: ResolverContext;
35
35
  readonly traceRecorder: TraceRecorder;
36
36
  };
37
- export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
37
+ export type ResolverAdapterFactory = (configuration: string | undefined, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
38
38
  export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
39
39
  export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
40
40
  /** Internal test seam; deliberately not re-exported from the package root. */
@@ -56,6 +56,9 @@ const SAFE_CAUSE_MESSAGE_WORDS = new Set([
56
56
  ]);
57
57
  const resolverAdapterRegistry = {
58
58
  "2captcha"(configuration, timeoutMs, allowedHosts) {
59
+ if (configuration === undefined) {
60
+ throw new Error("2captcha resolver adapter factory requires an API key");
61
+ }
59
62
  return createTwoCaptchaResolverVendorAdapter({
60
63
  allowedHosts,
61
64
  apiKey: configuration,
@@ -643,13 +646,18 @@ function resolveVendorAvailability(vendor, env) {
643
646
  reason: "missing_transport",
644
647
  };
645
648
  }
649
+ if (vendor === "browser") {
650
+ return {
651
+ vendor,
652
+ available: true,
653
+ configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
654
+ };
655
+ }
646
656
  const envKey = vendor === "2captcha"
647
657
  ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
648
658
  : vendor === "capsolver"
649
659
  ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
650
- : vendor === "capmonster"
651
- ? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
652
- : APIFUSE__CDP_POOL__URL;
660
+ : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
653
661
  const configuration = normalizedEnvValue(env, envKey);
654
662
  return configuration
655
663
  ? { vendor, available: true, configuration }