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

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/error-resolution.js +0 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/provider.d.ts +1 -1
  6. package/dist/provider.js +1 -1
  7. package/dist/runtime/auth-flow.d.ts +3 -1
  8. package/dist/runtime/auth-flow.js +1 -0
  9. package/dist/runtime/browser.d.ts +1 -0
  10. package/dist/runtime/browser.js +350 -27
  11. package/dist/runtime/choice.d.ts +0 -1
  12. package/dist/runtime/choice.js +10 -126
  13. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  14. package/dist/runtime/resolver-vendors/browser.js +68 -16
  15. package/dist/runtime/resolver-vendors/capsolver.d.ts +1 -3
  16. package/dist/runtime/resolver-vendors/capsolver.js +148 -24
  17. package/dist/runtime/resolver-vendors/twocaptcha.js +57 -18
  18. package/dist/runtime/resolver-vendors/types.d.ts +5 -2
  19. package/dist/runtime/resolver-vendors/types.js +16 -4
  20. package/dist/runtime/resolver.d.ts +1 -1
  21. package/dist/runtime/resolver.js +24 -5
  22. package/dist/server/serve-implementation.d.ts +2 -1
  23. package/dist/server/serve-implementation.js +27 -17
  24. package/dist/testing/run.js +1 -0
  25. package/dist/types.d.ts +25 -3
  26. package/package.json +3 -2
  27. package/src/error-resolution.ts +0 -1
  28. package/src/index.ts +0 -1
  29. package/src/provider.ts +0 -1
  30. package/src/runtime/auth-flow.ts +4 -0
  31. package/src/runtime/browser.ts +438 -31
  32. package/src/runtime/choice.ts +10 -151
  33. package/src/runtime/resolver-vendors/browser.ts +83 -16
  34. package/src/runtime/resolver-vendors/capsolver.ts +170 -33
  35. package/src/runtime/resolver-vendors/twocaptcha.ts +54 -15
  36. package/src/runtime/resolver-vendors/types.ts +22 -4
  37. package/src/runtime/resolver.ts +31 -7
  38. package/src/server/serve-implementation.ts +74 -17
  39. package/src/testing/run.ts +1 -0
  40. package/src/types.ts +26 -3
@@ -39,8 +39,6 @@ import type {
39
39
 
40
40
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
41
41
  "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
42
- export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV =
43
- "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
44
42
 
45
43
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
46
44
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
@@ -133,7 +131,6 @@ export function createProviderChoiceContext(
133
131
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
134
132
  const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
135
133
  if (resolvedStorage.mode === "server") {
136
- const issuance = resolveChoiceWordIssuance(options.env);
137
134
  const keys = hasRequestedChoiceBinding(issueOptions.bind)
138
135
  ? deriveManagedChoiceKeys({
139
136
  masterSecret: resolveMasterSecret(),
@@ -159,29 +156,8 @@ export function createProviderChoiceContext(
159
156
  ttl_ms: issueOptions.ttlMs,
160
157
  binding,
161
158
  } satisfies Omit<ManagedChoiceEnvelope, "payload">;
162
- if (issuance === "legacy") {
163
- const legacyKeys =
164
- keys ??
165
- deriveManagedChoiceKeys({
166
- masterSecret: resolveMasterSecret(),
167
- providerId: options.providerId,
168
- purpose: issueOptions.purpose,
169
- kid,
170
- });
171
- return issueLegacyServerStoredChoice({
172
- baseEnvelope,
173
- issueOptions,
174
- storage: resolvedStorage.storage,
175
- contextState: options.state,
176
- kid,
177
- keys: legacyKeys,
178
- issuedAtMs,
179
- });
180
- }
181
159
  return issueWordServerStoredChoice({
182
- baseEnvelope: {
183
- ...baseEnvelope,
184
- },
160
+ baseEnvelope,
185
161
  issueOptions,
186
162
  storage: resolvedStorage.storage,
187
163
  contextState: options.state,
@@ -287,10 +263,9 @@ export function createProviderChoiceContext(
287
263
  );
288
264
  }
289
265
 
290
- // Legacy encrypted-envelope compatibility fallback. Removal is gated on
291
- // the last legacy mint plus the maximum issued TTL; see ADR 0006.
292
- // A structurally valid word token returns above, so lookup, expiry,
293
- // consumption, and binding failures can never enter this branch.
266
+ // Inline choices continue to use the encrypted envelope. A structurally
267
+ // valid word token returns above, so lookup, expiry, consumption, and
268
+ // binding failures can never enter this branch.
294
269
  try {
295
270
  const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
296
271
  parseManagedChoiceTokenParts(parseOptions.token);
@@ -345,17 +320,14 @@ export function createProviderChoiceContext(
345
320
  required: true,
346
321
  }),
347
322
  });
348
- const payload = isServerChoiceHandlePayload(envelope.payload)
349
- ? parseLegacyServerStoredChoice({
350
- handle: envelope.payload,
351
- storage: parseOptions.storage,
352
- contextState: options.state,
353
- })
354
- : envelope.payload;
323
+ if (isServerChoiceHandlePayload(envelope.payload)) {
324
+ throw wordChoiceNotFoundError();
325
+ }
326
+ const payload = envelope.payload;
355
327
  const parsed =
356
328
  consumeMode === "explicit"
357
329
  ? Promise.resolve(payload).then((resolvedPayload) =>
358
- createLegacyExplicitParseResult({
330
+ createInlineExplicitParseResult({
359
331
  payload: resolvedPayload,
360
332
  replayKey: digestChoiceReplayKey(parseOptions.token),
361
333
  onConsume: () =>
@@ -495,7 +467,7 @@ function emitChoiceTelemetry(
495
467
  }
496
468
  }
497
469
 
498
- function createLegacyExplicitParseResult(options: {
470
+ function createInlineExplicitParseResult(options: {
499
471
  readonly payload: ProviderChoiceTokenPayload;
500
472
  readonly replayKey: string;
501
473
  readonly onConsume: () => void;
@@ -511,22 +483,6 @@ function createLegacyExplicitParseResult(options: {
511
483
  };
512
484
  }
513
485
 
514
- function resolveChoiceWordIssuance(env?: EnvContext): "legacy" | "word" {
515
- const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
516
- const value = configured?.trim() ?? "";
517
- if (value === "" || value === "legacy") return "legacy";
518
- if (value === "word") return "word";
519
- throw new ProviderError(
520
- `Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`,
521
- {
522
- code: "CHOICE_WORD_ISSUANCE_INVALID",
523
- category: "input_validation",
524
- retryable: false,
525
- details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
526
- },
527
- );
528
- }
529
-
530
486
  function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
531
487
  const configured =
532
488
  options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
@@ -657,56 +613,6 @@ async function issueWordServerStoredChoice<TPayload extends ProviderChoiceTokenP
657
613
  });
658
614
  }
659
615
 
660
- /**
661
- * Beta.28-compatible server handle issuance. The state stores the payload and
662
- * the client receives the existing six-part encrypted envelope whose payload
663
- * is a server-state handle.
664
- */
665
- async function issueLegacyServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
666
- readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
667
- readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
668
- readonly storage: ServerProviderChoiceStorageOptions;
669
- readonly contextState?: ProviderRuntimeState;
670
- readonly kid: string;
671
- readonly keys: ManagedChoiceKeys;
672
- readonly issuedAtMs: number;
673
- }): Promise<string> {
674
- const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
675
- const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
676
- if (payloadBytes > options.storage.maxValueBytes) {
677
- throw new ProviderError("Provider choice payload exceeds state storage policy.", {
678
- code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
679
- category: "input_validation",
680
- retryable: false,
681
- details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
682
- });
683
- }
684
- const stateId = `choice_${randomBytes(16).toString("base64url")}`;
685
- const namespace = resolveChoiceStateNamespace({
686
- storage: options.storage,
687
- contextState: options.contextState,
688
- ttlMs: options.issueOptions.ttlMs,
689
- });
690
- await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
691
- ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
692
- });
693
- const envelope: ManagedChoiceEnvelope = {
694
- ...options.baseEnvelope,
695
- payload: {
696
- storage: "server",
697
- state_id: stateId,
698
- payload_digest: digestChoicePayload(serializedPayload),
699
- created_at_ms: options.issuedAtMs,
700
- },
701
- };
702
- return encryptManagedChoiceToken({
703
- prefix: options.issueOptions.prefix,
704
- kid: options.kid,
705
- envelope,
706
- keys: options.keys,
707
- });
708
- }
709
-
710
616
  async function parseWordServerStoredChoice(options: {
711
617
  readonly stateKey: string;
712
618
  readonly parseOptions: ProviderChoiceParseOptions;
@@ -849,53 +755,6 @@ async function consumeWordServerStoredChoice(options: {
849
755
  }
850
756
  }
851
757
 
852
- async function parseLegacyServerStoredChoice(options: {
853
- readonly handle: ServerChoiceHandlePayload;
854
- readonly storage?: ProviderChoiceStorageOptions;
855
- readonly contextState?: ProviderRuntimeState;
856
- }): Promise<ProviderChoiceTokenPayload> {
857
- const storage = resolveParseStorage(options.storage);
858
- const namespace = resolveChoiceStateNamespace({
859
- storage,
860
- contextState: options.contextState,
861
- });
862
- // Reading a server-stored choice back deserializes a persisted value. A
863
- // corrupt/undecodable value would otherwise surface as a raw JSON.parse
864
- // SyntaxError (or another unexpected throwable) that escapes the choice error
865
- // taxonomy, gets masked as internal_error 500, and is treated as retryable by
866
- // the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
867
- // Convert any non-branded throwable into a branded invalid_payload so it maps
868
- // to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
869
- // ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
870
- // their category/retryable semantics are preserved.
871
- let record: StateValue<ProviderChoiceTokenPayload> | null;
872
- try {
873
- record = await namespace.get<ProviderChoiceTokenPayload>(
874
- optionsStateKey(options.handle.state_id),
875
- );
876
- } catch (error) {
877
- if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
878
- throw error;
879
- }
880
- throw new ProviderChoiceTokenError(
881
- "invalid_payload",
882
- "Provider choice token state payload could not be decoded.",
883
- );
884
- }
885
- if (!record) {
886
- throw new ProviderChoiceTokenError(
887
- "invalid_payload",
888
- "Provider choice token state payload is missing.",
889
- );
890
- }
891
- const serializedPayload = serializeChoicePayload(record.value);
892
- assertPayloadDigestMatches({
893
- actual: digestChoicePayload(serializedPayload),
894
- expected: options.handle.payload_digest,
895
- });
896
- return record.value;
897
- }
898
-
899
758
  function generateChoiceWordSequence(wordCount: number): string {
900
759
  return Array.from({ length: wordCount }, () =>
901
760
  choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
@@ -18,6 +18,8 @@ import {
18
18
 
19
19
  const BROWSER_VENDOR_ID = "browser" as const;
20
20
  const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
21
+ const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
22
+ const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
21
23
 
22
24
  const SUCCESS_COOKIE_NAMES = {
23
25
  aws_waf: "aws-waf-token",
@@ -28,6 +30,22 @@ type SupportedBrowserChallengeKind = keyof typeof SUCCESS_COOKIE_NAMES;
28
30
 
29
31
  type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
30
32
 
33
+ let createResolverBrowserClient: BrowserClientFactory = createBrowserClient;
34
+
35
+ /** Internal test seam; deliberately not re-exported from the package root. */
36
+ export function swapBrowserResolverClientFactoryForTests(
37
+ factory: BrowserClientFactory | undefined,
38
+ ): () => void {
39
+ const original = createResolverBrowserClient;
40
+ createResolverBrowserClient = factory ?? createBrowserClient;
41
+ let restored = false;
42
+ return () => {
43
+ if (restored) return;
44
+ restored = true;
45
+ createResolverBrowserClient = original;
46
+ };
47
+ }
48
+
31
49
  export interface BrowserResolverVendorOptions {
32
50
  readonly cdpUrl?: string;
33
51
  readonly timeoutMs: number;
@@ -200,31 +218,77 @@ function selectSuccessCookie(
200
218
 
201
219
  async function solveInPage(
202
220
  page: BrowserPage,
221
+ challengeKind: SupportedBrowserChallengeKind,
203
222
  pageUrl: string,
223
+ allowedHosts: readonly string[],
204
224
  successCookieName: string,
205
225
  pollIntervalMs: number,
206
226
  signal: AbortSignal,
207
227
  ): Promise<Extract<ChallengeSolution, { readonly form: "cookies" }>> {
208
- await raceWithAbort(() => page.goto(pageUrl), signal);
209
-
210
- while (true) {
211
- const cookies = await raceWithAbort(() => page.cookies(), signal);
212
- const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
213
- if (successCookie) {
228
+ return await page.withResourcePolicy(
229
+ {
230
+ allowedMethods: ["GET", "HEAD", "POST"],
231
+ documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
232
+ routes: [
233
+ {
234
+ match: () => true,
235
+ handle: (request) => ({
236
+ action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
237
+ ? "continue"
238
+ : "block",
239
+ }),
240
+ },
241
+ ],
242
+ },
243
+ async () => {
214
244
  const userAgent = await raceWithAbort(
215
- () => page.evaluate<string>("navigator.userAgent"),
245
+ () => page.userAgent(),
216
246
  signal,
217
247
  );
218
- return {
219
- form: "cookies",
220
- cookies: { [successCookieName]: successCookie.value },
221
- userAgent,
222
- ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
223
- };
224
- }
248
+ await raceWithAbort(() => page.goto(pageUrl), signal);
249
+
250
+ while (true) {
251
+ const cookies = await raceWithAbort(() => page.cookies(), signal);
252
+ const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
253
+ if (successCookie) {
254
+ return {
255
+ form: "cookies",
256
+ cookies: { [successCookieName]: successCookie.value },
257
+ userAgent,
258
+ ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
259
+ };
260
+ }
261
+
262
+ await abortableDelay(pollIntervalMs, signal);
263
+ }
264
+ },
265
+ );
266
+ }
225
267
 
226
- await abortableDelay(pollIntervalMs, signal);
268
+ function isResolverBrowserRequestAllowed(
269
+ targetUrl: string,
270
+ challengeKind: SupportedBrowserChallengeKind,
271
+ allowedHosts: readonly string[],
272
+ ): boolean {
273
+ try {
274
+ assertResolverHostAllowed(targetUrl, allowedHosts);
275
+ return true;
276
+ } catch {
277
+ if (challengeKind !== "aws_waf") return false;
227
278
  }
279
+
280
+ let target: URL;
281
+ try {
282
+ target = new URL(targetUrl);
283
+ } catch {
284
+ return false;
285
+ }
286
+ const hostname = normalizedResolverHostname(target.hostname);
287
+ return (
288
+ target.protocol === "https:" &&
289
+ hostname.length > AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX.length &&
290
+ hostname.endsWith(AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX)
291
+ );
228
292
  }
229
293
 
230
294
  const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
@@ -297,7 +361,7 @@ async function closeBrowserClient(
297
361
  export function createBrowserResolverVendorAdapter(
298
362
  options: BrowserResolverVendorOptions,
299
363
  ): BrowserResolverVendorAdapter {
300
- const createClient = options.createClient ?? createBrowserClient;
364
+ const createClient = options.createClient ?? createResolverBrowserClient;
301
365
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
302
366
 
303
367
  return {
@@ -352,12 +416,15 @@ export function createBrowserResolverVendorAdapter(
352
416
  cdpUrl: cdpUrl ?? "",
353
417
  ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
354
418
  requireCdpPool: cdpUrl !== undefined,
419
+ serviceWorkers: "block",
355
420
  });
356
421
  const contextOperation = client.withIsolatedContext(async (page) => {
357
422
  handlerEntered = true;
358
423
  return await solveInPage(
359
424
  page,
425
+ challengeKind,
360
426
  challenge.pageUrl,
427
+ options.allowedHosts,
361
428
  SUCCESS_COOKIE_NAMES[challengeKind],
362
429
  pollIntervalMs,
363
430
  solveController.signal,
@@ -1,5 +1,7 @@
1
+ import { getStealthProfile } from "../../stealth/profiles.js";
1
2
  import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
3
  import { redactSensitiveText } from "../request-options.js";
4
+ import { DEFAULT_PROFILE } from "../stealth.js";
3
5
  import type { TraceRecorder } from "../trace.js";
4
6
  import { assertResolverHostAllowed } from "./hosts.js";
5
7
  import {
@@ -39,7 +41,7 @@ export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
39
41
  identity: ResolverIdentity | undefined,
40
42
  signal: AbortSignal,
41
43
  traceRecorder?: TraceRecorder,
42
- ): Promise<Extract<ChallengeSolution, { readonly form: "token" }>>;
44
+ ): Promise<ChallengeSolution>;
43
45
  }
44
46
 
45
47
  class CapsolverSolveTimeoutError extends Error {
@@ -91,7 +93,15 @@ interface CapsolverCreateTaskResponse extends CapsolverResponseErrorFields {
91
93
 
92
94
  interface CapsolverPollResultResponse extends CapsolverResponseErrorFields {
93
95
  readonly status: string | undefined;
94
- readonly solution: { readonly token: string | undefined } | undefined;
96
+ readonly solution:
97
+ | {
98
+ readonly token: string | undefined;
99
+ readonly cookie: string | undefined;
100
+ readonly gRecaptchaResponse: string | undefined;
101
+ readonly cookies: Readonly<Record<string, string>> | undefined;
102
+ readonly userAgent: string | undefined;
103
+ }
104
+ | undefined;
95
105
  }
96
106
 
97
107
  class CapsolverVerdictError extends ResolverChallengeVerdictError {
@@ -233,15 +243,52 @@ function parseCreateTaskResponse(payload: JsonRecord): CapsolverCreateTaskRespon
233
243
 
234
244
  function parsePollResultResponse(payload: JsonRecord): CapsolverPollResultResponse {
235
245
  const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
246
+ const cookies = solution && isJsonRecord(solution.cookies)
247
+ ? Object.fromEntries(
248
+ Object.entries(solution.cookies).filter(
249
+ (entry): entry is [string, string] => typeof entry[1] === "string",
250
+ ),
251
+ )
252
+ : undefined;
236
253
  return {
237
254
  ...responseErrorFields(payload),
238
255
  status: typeof payload.status === "string" ? payload.status : undefined,
239
- solution: solution
240
- ? { token: typeof solution.token === "string" ? solution.token : undefined }
256
+ solution: solution
257
+ ? {
258
+ token: typeof solution.token === "string" ? solution.token : undefined,
259
+ cookie: typeof solution.cookie === "string" ? solution.cookie : undefined,
260
+ gRecaptchaResponse:
261
+ typeof solution.gRecaptchaResponse === "string"
262
+ ? solution.gRecaptchaResponse
263
+ : undefined,
264
+ cookies,
265
+ userAgent: typeof solution.userAgent === "string" ? solution.userAgent : undefined,
266
+ }
241
267
  : undefined,
242
268
  };
243
269
  }
244
270
 
271
+ function proxyForCapsolver(
272
+ proxyUrl: string,
273
+ ): { readonly value: string; readonly sensitive: readonly string[] } | undefined {
274
+ try {
275
+ const url = new URL(proxyUrl);
276
+ const protocol = url.protocol.slice(0, -1).toLowerCase();
277
+ const scheme = protocol === "http" || protocol === "socks5" ? protocol : undefined;
278
+ const port = Number(url.port || (scheme === "socks5" ? 1080 : 80));
279
+ if (!scheme || !url.hostname || !Number.isInteger(port) || port <= 0 || port > 65_535) {
280
+ return undefined;
281
+ }
282
+ const username = url.username ? decodeURIComponent(url.username) : "";
283
+ const password = url.password ? decodeURIComponent(url.password) : "";
284
+ if (username.includes(":") || password.includes(":")) return undefined;
285
+ const value = `${scheme}:${url.hostname}:${port}${username || password ? `:${username}:${password}` : ""}`;
286
+ return { value, sensitive: [proxyUrl, value, username, password].filter(Boolean) };
287
+ } catch {
288
+ return undefined;
289
+ }
290
+ }
291
+
245
292
  function isAllocationExhausted(payload: CapsolverResponseErrorFields): boolean {
246
293
  const code = payload.errorCode?.toLowerCase() ?? "";
247
294
  const description = payload.errorDescription?.toLowerCase() ?? "";
@@ -403,7 +450,7 @@ export function createCapsolverResolverVendorAdapter(
403
450
  return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
404
451
  },
405
452
 
406
- async solve(challenge, _identity, callerSignal, traceRecorder) {
453
+ async solve(challenge, identity, callerSignal, traceRecorder) {
407
454
  const apiKey = options.apiKey?.trim();
408
455
  if (!apiKey) {
409
456
  throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
@@ -413,17 +460,34 @@ export function createCapsolverResolverVendorAdapter(
413
460
  if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
414
461
  throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
415
462
  }
416
- if (challenge.kind !== "turnstile") {
417
- throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "not_implemented", {
463
+ const proxy = identity ? proxyForCapsolver(identity.proxyUrl) : undefined;
464
+ if (challenge.kind === "aws_waf" && identity && !proxy) {
465
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
418
466
  phase: "create_task",
419
467
  });
420
468
  }
469
+ if (challenge.kind === "cloudflare_interstitial" && !identity) {
470
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_proxy_identity", {
471
+ phase: "create_task",
472
+ });
473
+ }
474
+ if (identity && !proxy) {
475
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
476
+ phase: "create_task",
477
+ });
478
+ }
479
+ const challengeFields = challenge as unknown as Record<string, unknown>;
421
480
  const sensitiveValues = [
422
481
  apiKey,
423
482
  challenge.pageUrl,
424
- challenge.siteKey,
425
- ...(challenge.action !== undefined ? [challenge.action] : []),
426
- ...(challenge.cdata !== undefined ? [challenge.cdata] : []),
483
+ ...(typeof challengeFields.siteKey === "string" ? [challengeFields.siteKey] : []),
484
+ ...(typeof challengeFields.action === "string" ? [challengeFields.action] : []),
485
+ ...(typeof challengeFields.cdata === "string" ? [challengeFields.cdata] : []),
486
+ ...(typeof challengeFields.blockedHtml === "string" ? [challengeFields.blockedHtml] : []),
487
+ ...(typeof challengeFields.captchaScript === "string" ? [challengeFields.captchaScript] : []),
488
+ ...(typeof challengeFields.context === "string" ? [challengeFields.context] : []),
489
+ ...(typeof challengeFields.iv === "string" ? [challengeFields.iv] : []),
490
+ ...(proxy?.sensitive ?? []),
427
491
  ];
428
492
 
429
493
  assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
@@ -440,24 +504,73 @@ export function createCapsolverResolverVendorAdapter(
440
504
 
441
505
  try {
442
506
  const createTask = async () => {
443
- const metadata = {
444
- ...(challenge.action !== undefined ? { action: challenge.action } : {}),
445
- ...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
446
- };
507
+ const task =
508
+ challenge.kind === "aws_waf"
509
+ ? {
510
+ type: proxy ? "AntiAwsWafTask" : "AntiAwsWafTaskProxyLess",
511
+ websiteURL: challenge.pageUrl,
512
+ ...(challenge.siteKey !== undefined ? { awsKey: challenge.siteKey } : {}),
513
+ ...(challenge.iv !== undefined ? { awsIv: challenge.iv } : {}),
514
+ ...(challenge.context !== undefined ? { awsContext: challenge.context } : {}),
515
+ ...(challenge.captchaScript !== undefined
516
+ ? { awsChallengeJS: challenge.captchaScript }
517
+ : {}),
518
+ ...(proxy ? { proxy: proxy.value } : {}),
519
+ }
520
+ : challenge.kind === "turnstile"
521
+ ? {
522
+ type: "AntiTurnstileTaskProxyLess",
523
+ websiteURL: challenge.pageUrl,
524
+ websiteKey: challenge.siteKey,
525
+ ...(challenge.action !== undefined || challenge.cdata !== undefined
526
+ ? {
527
+ metadata: {
528
+ ...(challenge.action !== undefined ? { action: challenge.action } : {}),
529
+ ...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
530
+ },
531
+ }
532
+ : {}),
533
+ }
534
+ : challenge.kind === "recaptcha_v2"
535
+ ? {
536
+ type: proxy ? "ReCaptchaV2Task" : "ReCaptchaV2TaskProxyLess",
537
+ websiteURL: challenge.pageUrl,
538
+ websiteKey: challenge.siteKey,
539
+ ...(proxy ? { proxy: proxy.value } : {}),
540
+ }
541
+ : challenge.kind === "recaptcha_v3"
542
+ ? {
543
+ type: proxy ? "ReCaptchaV3Task" : "ReCaptchaV3TaskProxyLess",
544
+ websiteURL: challenge.pageUrl,
545
+ websiteKey: challenge.siteKey,
546
+ pageAction: challenge.action,
547
+ ...(challenge.minScore !== undefined ? { minScore: challenge.minScore } : {}),
548
+ ...(proxy ? { proxy: proxy.value } : {}),
549
+ }
550
+ : challenge.kind === "hcaptcha"
551
+ ? {
552
+ type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyLess",
553
+ websiteURL: challenge.pageUrl,
554
+ websiteKey: challenge.siteKey,
555
+ ...(proxy ? { proxy: proxy.value } : {}),
556
+ }
557
+ : challenge.kind === "cloudflare_interstitial"
558
+ ? {
559
+ type: "AntiCloudflareTask",
560
+ websiteURL: challenge.pageUrl,
561
+ proxy: proxy?.value,
562
+ ...(identity?.userAgent ? { userAgent: identity.userAgent } : {}),
563
+ ...(challenge.kind === "cloudflare_interstitial" && challenge.blockedHtml !== undefined
564
+ ? { html: challenge.blockedHtml }
565
+ : {}),
566
+ }
567
+ : (() => {
568
+ throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
569
+ })();
447
570
  const createResult = await postJson(
448
571
  fetchImpl,
449
572
  endpoint(baseUrl, "createTask"),
450
- {
451
- clientKey: apiKey,
452
- task: {
453
- type: "AntiTurnstileTaskProxyLess",
454
- websiteURL: challenge.pageUrl,
455
- websiteKey: challenge.siteKey,
456
- ...(challenge.action !== undefined || challenge.cdata !== undefined
457
- ? { metadata }
458
- : {}),
459
- },
460
- },
573
+ { clientKey: apiKey, task },
461
574
  solveController.signal,
462
575
  phase,
463
576
  sensitiveValues,
@@ -508,20 +621,44 @@ export function createCapsolverResolverVendorAdapter(
508
621
  case "ready":
509
622
  break;
510
623
  default:
511
- throw new ResolverVendorUnavailableError(
512
- CAPSOLVER_VENDOR_ID,
513
- "transport_failure",
514
- { phase },
515
- );
624
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
625
+ phase,
626
+ });
516
627
  }
517
628
 
518
- const token = result.payload.solution?.token;
519
- if (!token?.trim()) {
629
+ const solutionValue =
630
+ challenge.kind === "aws_waf"
631
+ ? result.payload.solution?.cookie
632
+ : result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse;
633
+ if (challenge.kind === "cloudflare_interstitial") {
634
+ const cookies = result.payload.solution?.cookies;
635
+ const clearance = cookies?.cf_clearance ?? solutionValue;
636
+ if (!clearance?.trim()) {
637
+ throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
638
+ phase,
639
+ });
640
+ }
641
+ return {
642
+ form: "cookies" as const,
643
+ cookies: cookies && Object.keys(cookies).length > 0 ? cookies : { cf_clearance: clearance },
644
+ userAgent:
645
+ result.payload.solution?.userAgent ??
646
+ identity?.userAgent ??
647
+ getStealthProfile(DEFAULT_PROFILE).userAgent,
648
+ };
649
+ }
650
+ if (!solutionValue?.trim()) {
520
651
  throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
521
652
  phase,
522
653
  });
523
654
  }
524
- return { form: "token" as const, token };
655
+ return challenge.kind === "aws_waf"
656
+ ? {
657
+ form: "cookies" as const,
658
+ cookies: { "aws-waf-token": solutionValue },
659
+ userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
660
+ }
661
+ : { form: "token" as const, token: solutionValue };
525
662
  }
526
663
  };
527
664
  return traceRecorder