@apifuse/provider-sdk 2.2.0-beta.27 → 2.2.0-beta.29

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 (98) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +34 -5
  3. package/bin/apifuse-pack-smoke.ts +1 -1
  4. package/bin/apifuse-pack-types.ts +26 -2
  5. package/bin/apifuse-perf.ts +2 -4
  6. package/bin/apifuse-record.ts +39 -6
  7. package/dist/auth-turn/index.d.ts +1 -1
  8. package/dist/auth-turn/index.js +1 -1
  9. package/dist/auth.d.ts +14 -0
  10. package/dist/auth.js +38 -0
  11. package/dist/ceremonies/index.js +52 -11
  12. package/dist/config/loader.d.ts +3 -1
  13. package/dist/config/loader.js +4 -2
  14. package/dist/index.d.ts +8 -7
  15. package/dist/index.js +5 -7
  16. package/dist/provider.d.ts +2 -1
  17. package/dist/provider.js +1 -1
  18. package/dist/runtime/auth-flow.js +1 -1
  19. package/dist/runtime/browser.js +45 -2
  20. package/dist/runtime/http.d.ts +1 -0
  21. package/dist/runtime/http.js +135 -12
  22. package/dist/runtime/instrumentation.js +1 -1
  23. package/dist/runtime/native-network-errors.d.ts +33 -0
  24. package/dist/runtime/native-network-errors.js +69 -0
  25. package/dist/runtime/native-network.d.ts +2 -33
  26. package/dist/runtime/native-network.js +2 -68
  27. package/dist/runtime/proxy-telemetry.js +3 -0
  28. package/dist/runtime/redis.d.ts +1 -1
  29. package/dist/runtime/redis.js +4 -2
  30. package/dist/runtime/resolver-config.d.ts +6 -0
  31. package/dist/runtime/resolver-config.js +6 -0
  32. package/dist/runtime/resolver-public.d.ts +1 -0
  33. package/dist/runtime/resolver-public.js +1 -0
  34. package/dist/runtime/resolver-shared.d.ts +3 -0
  35. package/dist/runtime/resolver-shared.js +12 -0
  36. package/dist/runtime/resolver-vendors/browser.js +14 -4
  37. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
  38. package/dist/runtime/resolver-vendors/twocaptcha.js +157 -53
  39. package/dist/runtime/resolver-vendors/types.d.ts +2 -2
  40. package/dist/runtime/resolver-vendors/types.js +3 -1
  41. package/dist/runtime/resolver.d.ts +15 -10
  42. package/dist/runtime/resolver.js +92 -23
  43. package/dist/runtime/state.js +5 -115
  44. package/dist/runtime/stealth-cookies.d.ts +20 -0
  45. package/dist/runtime/stealth-cookies.js +111 -0
  46. package/dist/runtime/stealth.d.ts +1 -0
  47. package/dist/runtime/stealth.js +8 -131
  48. package/dist/serve.d.ts +1 -1
  49. package/dist/serve.js +1 -1
  50. package/dist/server/index.d.ts +1 -1
  51. package/dist/server/index.js +1 -1
  52. package/dist/server/self-test.d.ts +13 -0
  53. package/dist/server/self-test.js +124 -46
  54. package/dist/server/serve-implementation.d.ts +199 -0
  55. package/dist/server/serve-implementation.js +2072 -0
  56. package/dist/server/serve.d.ts +1 -187
  57. package/dist/server/serve.js +1 -1827
  58. package/dist/stateful/errors.d.ts +5 -0
  59. package/dist/stateful/errors.js +10 -0
  60. package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
  61. package/dist/stateful/stateful-provider-session-routing.js +2 -10
  62. package/dist/stream.js +7 -1
  63. package/dist/testing/index.d.ts +1 -0
  64. package/dist/testing/index.js +1 -0
  65. package/package.json +27 -2
  66. package/src/auth-turn/index.ts +1 -1
  67. package/src/auth.ts +78 -0
  68. package/src/ceremonies/index.ts +68 -18
  69. package/src/config/loader.ts +8 -2
  70. package/src/index.ts +18 -24
  71. package/src/provider.ts +12 -14
  72. package/src/runtime/auth-flow.ts +1 -1
  73. package/src/runtime/browser.ts +50 -2
  74. package/src/runtime/http.ts +155 -11
  75. package/src/runtime/instrumentation.ts +1 -1
  76. package/src/runtime/native-network-errors.ts +99 -0
  77. package/src/runtime/native-network.ts +16 -97
  78. package/src/runtime/proxy-telemetry.ts +5 -0
  79. package/src/runtime/redis.ts +7 -2
  80. package/src/runtime/resolver-config.ts +6 -0
  81. package/src/runtime/resolver-public.ts +18 -0
  82. package/src/runtime/resolver-shared.ts +17 -0
  83. package/src/runtime/resolver-vendors/browser.ts +14 -4
  84. package/src/runtime/resolver-vendors/twocaptcha.ts +190 -56
  85. package/src/runtime/resolver-vendors/types.ts +8 -2
  86. package/src/runtime/resolver.ts +140 -28
  87. package/src/runtime/state.ts +5 -144
  88. package/src/runtime/stealth-cookies.ts +132 -0
  89. package/src/runtime/stealth.ts +15 -158
  90. package/src/serve.ts +6 -1
  91. package/src/server/index.ts +1 -0
  92. package/src/server/self-test.ts +184 -59
  93. package/src/server/serve-implementation.ts +3042 -0
  94. package/src/server/serve.ts +1 -2661
  95. package/src/stateful/errors.ts +12 -0
  96. package/src/stateful/stateful-provider-session-routing.ts +2 -11
  97. package/src/stream.ts +8 -1
  98. package/src/testing/index.ts +1 -0
@@ -0,0 +1,3 @@
1
+ import type { ResolverContext } from "../types.js";
2
+ export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
3
+ export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
@@ -0,0 +1,12 @@
1
+ import { ProviderError } from "../errors.js";
2
+ export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata");
3
+ export function createUnsupportedResolverClient(reason) {
4
+ return {
5
+ async solve() {
6
+ throw new ProviderError(reason ?? "Resolver runtime is not configured", {
7
+ code: "RESOLVER_UNAVAILABLE",
8
+ fix: "Declare resolver on the provider definition and configure vendor credentials.",
9
+ });
10
+ },
11
+ };
12
+ }
@@ -204,8 +204,9 @@ export function createBrowserResolverVendorAdapter(options) {
204
204
  });
205
205
  },
206
206
  async solve(challenge, identity, callerSignal, traceRecorder) {
207
- void identity;
208
- if (!options.cdpUrl?.trim()) {
207
+ const cdpUrl = options.cdpUrl?.trim();
208
+ const proxyUrl = identity?.proxyUrl;
209
+ if (!cdpUrl && proxyUrl === undefined) {
209
210
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_credentials");
210
211
  }
211
212
  if (!isSupportedKind(challenge.kind)) {
@@ -214,6 +215,14 @@ export function createBrowserResolverVendorAdapter(options) {
214
215
  assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
215
216
  const challengeKind = challenge.kind;
216
217
  callerSignal.throwIfAborted();
218
+ if (proxyUrl !== undefined && proxyUrl.length === 0) {
219
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_proxy_identity");
220
+ }
221
+ if (cdpUrl && proxyUrl !== undefined) {
222
+ // The current pool acquire protocol cannot bind a proxy to its browser context.
223
+ // Refuse so the chain can choose a vendor that can honor the resolved identity.
224
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
225
+ }
217
226
  const solveController = new AbortController();
218
227
  const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
219
228
  callerSignal.addEventListener("abort", onCallerAbort, { once: true });
@@ -223,8 +232,9 @@ export function createBrowserResolverVendorAdapter(options) {
223
232
  try {
224
233
  client = createClient({
225
234
  allowedHosts: [...options.allowedHosts],
226
- cdpUrl: options.cdpUrl.trim(),
227
- requireCdpPool: true,
235
+ cdpUrl: cdpUrl ?? "",
236
+ ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
237
+ requireCdpPool: cdpUrl !== undefined,
228
238
  });
229
239
  const contextOperation = client.withIsolatedContext(async (page) => {
230
240
  handlerEntered = true;
@@ -1,4 +1,5 @@
1
1
  import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
+ import type { TraceRecorder } from "../trace.js";
2
3
  import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
3
4
  type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
4
5
  export interface TwoCaptchaResolverVendorOptions {
@@ -15,7 +16,7 @@ export interface TwoCaptchaResolverVendorOptions {
15
16
  }
16
17
  export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
17
18
  readonly id: "2captcha";
18
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal): Promise<Extract<ChallengeSolution, {
19
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
19
20
  readonly form: "token";
20
21
  }>>;
21
22
  }
@@ -1,5 +1,5 @@
1
1
  import { assertResolverHostAllowed } from "./hosts.js";
2
- import { ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
2
+ import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
3
3
  const TWOCAPTCHA_VENDOR_ID = "2captcha";
4
4
  const DEFAULT_TWOCAPTCHA_BASE_URL = "https://api.2captcha.com";
5
5
  const DEFAULT_POLL_INTERVAL_MS = 3_000;
@@ -16,7 +16,46 @@ function isJsonRecord(value) {
16
16
  function abortReason(signal) {
17
17
  return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
18
18
  }
19
- function raceWithAbort(operation, signal, phase) {
19
+ function containsSensitiveValue(value, sensitiveValues) {
20
+ const secrets = sensitiveValues.filter((secret) => secret.length > 0);
21
+ if (secrets.length === 0)
22
+ return false;
23
+ const seen = new Set();
24
+ const inspect = (candidate) => {
25
+ if (typeof candidate === "string") {
26
+ return secrets.some((secret) => candidate.includes(secret));
27
+ }
28
+ if (candidate === null ||
29
+ (typeof candidate !== "object" && typeof candidate !== "function")) {
30
+ return false;
31
+ }
32
+ if (seen.has(candidate))
33
+ return false;
34
+ seen.add(candidate);
35
+ try {
36
+ for (const property of Reflect.ownKeys(candidate)) {
37
+ if (typeof property === "string" && inspect(property))
38
+ return true;
39
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
40
+ if (!descriptor)
41
+ return true;
42
+ if ("value" in descriptor && inspect(descriptor.value))
43
+ return true;
44
+ if (descriptor.get !== undefined || descriptor.set !== undefined)
45
+ return true;
46
+ }
47
+ }
48
+ catch {
49
+ return true;
50
+ }
51
+ return false;
52
+ };
53
+ return inspect(value);
54
+ }
55
+ function safeCauseOptions(error, sensitiveValues) {
56
+ return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
57
+ }
58
+ function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
20
59
  if (signal.aborted)
21
60
  return Promise.reject(abortReason(signal));
22
61
  return new Promise((resolve, reject) => {
@@ -36,7 +75,7 @@ function raceWithAbort(operation, signal, phase) {
36
75
  return;
37
76
  }
38
77
  reject(new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
39
- cause: error,
78
+ ...safeCauseOptions(error, sensitiveValues),
40
79
  phase,
41
80
  }));
42
81
  });
@@ -92,17 +131,23 @@ function isAllocationExhausted(payload) {
92
131
  return (code === "error_zero_balance" ||
93
132
  /(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
94
133
  }
134
+ function isNegativeVerdict(payload) {
135
+ return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
136
+ }
95
137
  function unavailableForPayload(payload, phase) {
138
+ if (isNegativeVerdict(payload)) {
139
+ return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
140
+ }
96
141
  return new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase });
97
142
  }
98
- async function postJson(fetchImpl, url, body, signal, phase) {
143
+ async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues) {
99
144
  const response = await raceWithAbort(() => fetchImpl(url, {
100
145
  method: "POST",
101
146
  headers: { "content-type": "application/json" },
102
147
  body: JSON.stringify(body),
103
148
  signal,
104
149
  redirect: "error",
105
- }), signal, phase);
150
+ }), signal, phase, sensitiveValues);
106
151
  let responseText;
107
152
  try {
108
153
  responseText = await raceWithAbort(() => response.text(), signal, phase);
@@ -136,10 +181,13 @@ function taskIdFrom(payload) {
136
181
  const taskId = payload.taskId;
137
182
  return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
138
183
  }
139
- function tokenFrom(payload) {
184
+ function tokenFrom(payload, challenge) {
140
185
  const solution = payload.solution;
141
186
  if (!isJsonRecord(solution))
142
187
  return undefined;
188
+ if (challenge.kind === "aws_waf") {
189
+ return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
190
+ }
143
191
  if (typeof solution.gRecaptchaResponse === "string")
144
192
  return solution.gRecaptchaResponse;
145
193
  return typeof solution.token === "string" ? solution.token : undefined;
@@ -147,6 +195,24 @@ function tokenFrom(payload) {
147
195
  function endpoint(baseUrl, path) {
148
196
  return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
149
197
  }
198
+ function spanErrorAttributes(error, phase) {
199
+ if (error instanceof ResolverVendorUnavailableError) {
200
+ return {
201
+ unavailability_reason: error.reason,
202
+ transport_phase: error.phase,
203
+ };
204
+ }
205
+ // The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
206
+ // the outer catch converts it only after the span has been finalized, so the span has
207
+ // to recognize it here or slow solves lose exactly the attribution these spans add.
208
+ if (error instanceof TwoCaptchaSolveTimeoutError) {
209
+ return {
210
+ unavailability_reason: "timeout",
211
+ transport_phase: phase,
212
+ };
213
+ }
214
+ return undefined;
215
+ }
150
216
  export function createTwoCaptchaResolverVendorAdapter(options) {
151
217
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
152
218
  const baseUrl = options.baseUrl ?? DEFAULT_TWOCAPTCHA_BASE_URL;
@@ -159,7 +225,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
159
225
  supports(kind) {
160
226
  return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
161
227
  },
162
- async solve(challenge, identity, callerSignal) {
228
+ async solve(challenge, identity, callerSignal, traceRecorder) {
163
229
  const apiKey = options.apiKey?.trim();
164
230
  if (!apiKey) {
165
231
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
@@ -169,8 +235,16 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
169
235
  if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
170
236
  throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
171
237
  }
172
- if (challenge.kind !== "recaptcha_v2") {
173
- // AWS WAF remains deferred because its challenge variant has no required site key.
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
+ if (challenge.kind === "aws_waf" &&
244
+ (!challenge.siteKey?.trim() ||
245
+ !challenge.captchaScript?.trim() ||
246
+ !challenge.context?.trim() ||
247
+ !challenge.iv?.trim())) {
174
248
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
175
249
  phase: "create_task",
176
250
  });
@@ -192,50 +266,78 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
192
266
  const startedAt = now();
193
267
  let phase = "create_task";
194
268
  try {
195
- const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
196
- clientKey: apiKey,
197
- task: {
198
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
199
- websiteURL: challenge.pageUrl,
200
- websiteKey: challenge.siteKey,
201
- isInvisible: false,
202
- ...(identity ? { userAgent: identity.userAgent } : {}),
203
- ...(proxy ?? {}),
204
- },
205
- }, solveController.signal, phase);
206
- const taskId = taskIdFrom(createResult.payload);
207
- if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
208
- throw unavailableForPayload(createResult.payload, phase);
209
- }
210
- phase = "poll_result";
211
- while (true) {
212
- callerSignal.throwIfAborted();
213
- const remainingMs = timeoutMs - (now() - startedAt);
214
- if (remainingMs <= 0)
215
- throw new TwoCaptchaSolveTimeoutError();
216
- await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
217
- callerSignal.throwIfAborted();
218
- if (now() - startedAt >= timeoutMs)
219
- throw new TwoCaptchaSolveTimeoutError();
220
- const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
221
- if (!pollResult.ok || pollResult.payload.errorId !== 0) {
222
- throw unavailableForPayload(pollResult.payload, phase);
223
- }
224
- if (pollResult.payload.status === "processing")
225
- continue;
226
- if (pollResult.payload.status !== "ready") {
227
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
228
- phase,
229
- });
269
+ const createTask = async () => {
270
+ const task = challenge.kind === "aws_waf"
271
+ ? {
272
+ type: proxy ? "AmazonTask" : "AmazonTaskProxyless",
273
+ websiteURL: challenge.pageUrl,
274
+ websiteKey: challenge.siteKey,
275
+ captchaScript: challenge.captchaScript,
276
+ context: challenge.context,
277
+ iv: challenge.iv,
278
+ ...(identity ? { userAgent: identity.userAgent } : {}),
279
+ ...(proxy ?? {}),
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
+ };
289
+ const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
290
+ const taskId = taskIdFrom(createResult.payload);
291
+ if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
292
+ throw unavailableForPayload(createResult.payload, phase);
230
293
  }
231
- const token = tokenFrom(pollResult.payload);
232
- if (!token?.trim()) {
233
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
234
- phase,
235
- });
294
+ return taskId;
295
+ };
296
+ const taskId = traceRecorder
297
+ ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
298
+ attributes: {
299
+ vendor: TWOCAPTCHA_VENDOR_ID,
300
+ challenge_kind: challenge.kind,
301
+ },
302
+ onError: (error) => spanErrorAttributes(error, "create_task"),
303
+ })
304
+ : await createTask();
305
+ phase = "poll_result";
306
+ const pollResult = async () => {
307
+ while (true) {
308
+ callerSignal.throwIfAborted();
309
+ const remainingMs = timeoutMs - (now() - startedAt);
310
+ if (remainingMs <= 0)
311
+ throw new TwoCaptchaSolveTimeoutError();
312
+ await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
313
+ callerSignal.throwIfAborted();
314
+ if (now() - startedAt >= timeoutMs)
315
+ throw new TwoCaptchaSolveTimeoutError();
316
+ const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, [apiKey]);
317
+ if (!pollResult.ok || pollResult.payload.errorId !== 0) {
318
+ throw unavailableForPayload(pollResult.payload, phase);
319
+ }
320
+ if (pollResult.payload.status === "processing")
321
+ continue;
322
+ if (pollResult.payload.status !== "ready") {
323
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
324
+ }
325
+ const token = tokenFrom(pollResult.payload, challenge);
326
+ if (!token?.trim()) {
327
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
328
+ }
329
+ return { form: "token", token };
236
330
  }
237
- return { form: "token", token };
238
- }
331
+ };
332
+ return traceRecorder
333
+ ? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
334
+ attributes: {
335
+ vendor: TWOCAPTCHA_VENDOR_ID,
336
+ challenge_kind: challenge.kind,
337
+ },
338
+ onError: (error) => spanErrorAttributes(error, "poll_result"),
339
+ })
340
+ : await pollResult();
239
341
  }
240
342
  catch (error) {
241
343
  if (callerSignal.aborted)
@@ -247,10 +349,12 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
247
349
  phase,
248
350
  });
249
351
  }
250
- if (error instanceof ResolverVendorUnavailableError)
352
+ if (error instanceof ResolverVendorUnavailableError ||
353
+ error instanceof ResolverChallengeVerdictError) {
251
354
  throw error;
355
+ }
252
356
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
253
- cause: error,
357
+ ...safeCauseOptions(error, [apiKey]),
254
358
  phase,
255
359
  });
256
360
  }
@@ -55,8 +55,8 @@ export interface ResolverVendorAdapter {
55
55
  getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
56
56
  solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport): Promise<ChallengeSolution>;
57
57
  }
58
- export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
59
- export type ResolverChallengeVerdictReason = "human_puzzle";
58
+ export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
59
+ export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
60
60
  type ResolverErrorOptions = {
61
61
  /** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
62
62
  readonly cause?: unknown;
@@ -56,7 +56,9 @@ export class ResolverChallengeVerdictError extends Error {
56
56
  vendor;
57
57
  reason;
58
58
  constructor(vendor, reason, options = {}) {
59
- super(`Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
59
+ super(reason === "solve_failed"
60
+ ? `Resolver vendor ${vendor} attempted the challenge but did not solve it`
61
+ : `Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
60
62
  this.vendor = vendor;
61
63
  this.reason = reason;
62
64
  this.name = "ResolverChallengeVerdictError";
@@ -1,12 +1,9 @@
1
- import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
1
+ import { type ProxyResolutionOptions } from "../config/loader.js";
2
+ import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderProxyMode, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
2
3
  import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorTransport } from "./resolver-vendors/types.js";
3
4
  import type { TraceRecorder } from "./trace.js";
4
- export declare const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
5
- export declare const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
6
- export declare const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
7
- export declare const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
8
- export declare const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
9
- export declare const DEFAULT_RESOLVER_TIMEOUT_MS = 180000;
5
+ export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
6
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
10
7
  type EnvLike = Record<string, string | undefined>;
11
8
  type ResolverChainClient = ResolverContext & {
12
9
  solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
@@ -14,6 +11,14 @@ type ResolverChainClient = ResolverContext & {
14
11
  export interface ResolverRuntimeOptions {
15
12
  readonly allowedHosts?: readonly string[];
16
13
  readonly cache?: ProviderCache;
14
+ /** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
15
+ readonly proxyIntent?: {
16
+ readonly mode: ProviderProxyMode;
17
+ readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
18
+ readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
19
+ readonly telemetry?: ProxyResolutionOptions["telemetry"];
20
+ readonly userAgent?: string;
21
+ };
17
22
  /** Server-owned context/proxy scope used only for identity-bound cache entries. */
18
23
  readonly identityScope?: string;
19
24
  /** SDK-owned transport already bound to the resolved proxy lease and client profile. */
@@ -25,7 +30,6 @@ export interface ResolverRuntimeOptions {
25
30
  readonly identityScope?: string;
26
31
  }) => ResolverVendorTransport;
27
32
  }
28
- export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
29
33
  export type ResolverInstrumentationMetadata = {
30
34
  readonly target: ResolverContext;
31
35
  readonly traceRecorder: TraceRecorder;
@@ -33,6 +37,8 @@ export type ResolverInstrumentationMetadata = {
33
37
  export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
34
38
  export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
35
39
  export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
40
+ /** Internal test seam; deliberately not re-exported from the package root. */
41
+ export declare function swapResolverDefaultUserAgentForTests(resolver: (() => string | undefined) | undefined): () => void;
36
42
  /** Remove the cached entry for the exact solution object returned by this resolver. */
37
43
  export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
38
44
  export declare function createResolverClient(options: {
@@ -41,14 +47,13 @@ export declare function createResolverClient(options: {
41
47
  readonly unavailableReason?: string;
42
48
  readonly cache?: ProviderCache;
43
49
  readonly identity?: ResolverIdentity;
50
+ readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
44
51
  readonly transport?: ResolverVendorTransport;
45
52
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
46
53
  readonly clientProfile?: string;
47
54
  readonly allowedHosts?: readonly string[];
48
55
  }): ResolverChainClient;
49
- export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
50
56
  export declare function bindResolverSignal(resolver: ResolverContext, defaultSignal: AbortSignal | undefined): ResolverContext;
51
57
  export declare function createResolverClientFromEnv(config: ProviderResolverConfig | undefined, env?: EnvLike, options?: ResolverRuntimeOptions): ResolverContext;
52
58
  /** Internal test seam; deliberately not re-exported from the package root. */
53
59
  export declare function createResolverClientFromEnvForTests(config: ProviderResolverConfig | undefined, env: EnvLike, options: ResolverRuntimeOptions, adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>): ResolverContext;
54
- export {};