@apifuse/provider-sdk 2.2.0-beta.36 → 2.2.0-beta.37

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 (37) hide show
  1. package/AUTHORING.md +22 -8
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +20 -8
  4. package/bin/apifuse-dev.ts +1 -1
  5. package/bin/apifuse-pack-types.ts +6 -5
  6. package/bin/apifuse-record.ts +1 -1
  7. package/bin/apifuse-submit-check.ts +23 -10
  8. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  9. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  10. package/dist/define.d.ts +39 -21
  11. package/dist/define.js +28 -9
  12. package/dist/index.d.ts +2 -2
  13. package/dist/provider.d.ts +2 -1
  14. package/dist/runtime/browser.js +19 -11
  15. package/dist/runtime/resolver-public.d.ts +1 -1
  16. package/dist/runtime/resolver-public.js +1 -1
  17. package/dist/runtime/resolver-vendors/browser.js +57 -14
  18. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  19. package/dist/runtime/resolver-vendors/types.js +15 -0
  20. package/dist/runtime/resolver.d.ts +1 -0
  21. package/dist/runtime/resolver.js +13 -7
  22. package/dist/server/serve-implementation.js +25 -0
  23. package/dist/types.d.ts +25 -17
  24. package/package.json +1 -1
  25. package/src/cli/templates/provider/index.ts.tpl +6 -3
  26. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  27. package/src/define.ts +135 -51
  28. package/src/index.ts +4 -2
  29. package/src/provider.ts +6 -1
  30. package/src/runtime/browser.ts +34 -11
  31. package/src/runtime/resolver-public.ts +2 -0
  32. package/src/runtime/resolver-vendors/browser.ts +69 -11
  33. package/src/runtime/resolver-vendors/types.ts +21 -0
  34. package/src/runtime/resolver.ts +17 -5
  35. package/src/server/serve-implementation.ts +39 -1
  36. package/src/testing/run.ts +3 -3
  37. package/src/types.ts +41 -17
@@ -548,8 +548,8 @@ class PlaywrightBrowserPage {
548
548
  this.page = page;
549
549
  this.proxy = proxy;
550
550
  }
551
- async goto(url) {
552
- await this.page.goto(url);
551
+ async goto(url, options) {
552
+ await this.page.goto(url, options);
553
553
  }
554
554
  async evaluate(fn) {
555
555
  if (typeof fn === "string") {
@@ -1101,16 +1101,22 @@ class CdpPoolBrowserPage {
1101
1101
  get id() {
1102
1102
  return this.pageId;
1103
1103
  }
1104
- async goto(url) {
1104
+ async goto(url, options) {
1105
1105
  await this.initialize();
1106
1106
  const startedAt = Date.now();
1107
- let loadEventSeen = false;
1108
- const unsubscribe = this.pageClient.on("Page.loadEventFired", () => {
1109
- loadEventSeen = true;
1107
+ const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
1108
+ const waitUntil = options?.waitUntil ?? "load";
1109
+ let expectedEventSeen = false;
1110
+ const eventName = waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
1111
+ const unsubscribe = this.pageClient.on(eventName, () => {
1112
+ expectedEventSeen = true;
1110
1113
  });
1111
1114
  try {
1112
- await this.pageClient.send("Page.navigate", { url });
1113
- await this.waitForDocumentReady(startedAt + DEFAULT_WAIT_TIMEOUT_MS, () => loadEventSeen);
1115
+ const navigation = (await this.pageClient.send("Page.navigate", { url }));
1116
+ if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
1117
+ throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
1118
+ }
1119
+ await this.waitForDocumentReady(startedAt + timeout, () => expectedEventSeen, waitUntil);
1114
1120
  }
1115
1121
  finally {
1116
1122
  unsubscribe();
@@ -1397,11 +1403,13 @@ class CdpPoolBrowserPage {
1397
1403
  this.frameExecutionContexts.set(frameId, contextId);
1398
1404
  return contextId;
1399
1405
  }
1400
- async waitForDocumentReady(deadline, isLoadEventSeen) {
1406
+ async waitForDocumentReady(deadline, isExpectedEventSeen, waitUntil) {
1401
1407
  while (Date.now() < deadline) {
1402
1408
  const readyState = await this.evaluate("document.readyState");
1403
- if (readyState === "complete" || readyState === "interactive") {
1404
- if (isLoadEventSeen() || readyState === "complete") {
1409
+ const documentReady = readyState === "complete" ||
1410
+ (waitUntil === "domcontentloaded" && readyState === "interactive");
1411
+ if (documentReady) {
1412
+ if (isExpectedEventSeen() || readyState === "complete") {
1405
1413
  return;
1406
1414
  }
1407
1415
  }
@@ -1 +1 @@
1
- export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
1
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
@@ -1 +1 @@
1
- export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver.js";
1
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, } from "./resolver.js";
@@ -5,6 +5,7 @@ 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 NAVIGATION_BLOCKED_ERROR_TEXT = "net::ERR_BLOCKED_BY_CLIENT";
8
9
  const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
9
10
  const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
10
11
  const SUCCESS_COOKIE_NAMES = {
@@ -25,11 +26,32 @@ export function swapBrowserResolverClientFactoryForTests(factory) {
25
26
  };
26
27
  }
27
28
  class BrowserSolveTimeoutError extends Error {
28
- constructor() {
29
- super("Browser resolver solve budget elapsed");
29
+ constructor(blockedRequests) {
30
+ super(`Browser resolver solve budget elapsed${formatBlockedRequests(blockedRequests)}`);
30
31
  this.name = "BrowserSolveTimeoutError";
31
32
  }
32
33
  }
34
+ class BrowserNavigationBlockedError extends Error {
35
+ navigationUrl;
36
+ blockedUrls;
37
+ code = "RESOLVER_BROWSER_NAVIGATION_BLOCKED";
38
+ constructor(navigationUrl, blockedUrls, options) {
39
+ super(`Browser resolver navigation was blocked for ${navigationUrl}${formatBlockedRequests(blockedUrls)}`, options);
40
+ this.navigationUrl = navigationUrl;
41
+ this.blockedUrls = blockedUrls;
42
+ this.name = "BrowserNavigationBlockedError";
43
+ }
44
+ }
45
+ function formatBlockedRequests(blockedRequests) {
46
+ if (blockedRequests.length === 0)
47
+ return "";
48
+ const displayed = blockedRequests.slice(0, 5);
49
+ const remainder = blockedRequests.length - displayed.length;
50
+ return `; blocked ${blockedRequests.length} requests: [${displayed.join(", ")}]${remainder > 0 ? ` (+${remainder} more)` : ""}`;
51
+ }
52
+ function isNavigationBlockedError(error) {
53
+ return error instanceof Error && error.message.includes(NAVIGATION_BLOCKED_ERROR_TEXT);
54
+ }
33
55
  class BrowserCleanupTimeoutError extends Error {
34
56
  constructor(timeoutMs) {
35
57
  super(`Browser resolver cleanup exceeded ${timeoutMs}ms`);
@@ -141,23 +163,35 @@ function selectSuccessCookie(cookies, successCookieName, pageUrl) {
141
163
  cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
142
164
  right.path.length - left.path.length)[0];
143
165
  }
144
- async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, signal) {
166
+ async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, gotoTimeoutMs, blockedRequests, signal) {
145
167
  return await page.withResourcePolicy({
146
168
  allowedMethods: ["GET", "HEAD", "POST"],
147
169
  documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
148
170
  routes: [
149
171
  {
150
172
  match: () => true,
151
- handle: (request) => ({
152
- action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
153
- ? "continue"
154
- : "block",
155
- }),
173
+ handle: (request) => {
174
+ if (isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)) {
175
+ return { action: "continue" };
176
+ }
177
+ blockedRequests.add(request.url);
178
+ return { action: "block" };
179
+ },
156
180
  },
157
181
  ],
158
182
  }, async () => {
159
183
  const userAgent = await raceWithAbort(() => page.userAgent(), signal);
160
- await raceWithAbort(() => page.goto(pageUrl), signal);
184
+ try {
185
+ await raceWithAbort(() => page.goto(pageUrl, {
186
+ timeout: gotoTimeoutMs,
187
+ waitUntil: "domcontentloaded",
188
+ }), signal);
189
+ }
190
+ catch (error) {
191
+ if (!isNavigationBlockedError(error))
192
+ throw error;
193
+ throw new BrowserNavigationBlockedError(pageUrl, [...blockedRequests], { cause: error });
194
+ }
161
195
  while (true) {
162
196
  const cookies = await raceWithAbort(() => page.cookies(), signal);
163
197
  const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
@@ -203,7 +237,7 @@ function poolErrorCode(error) {
203
237
  const code = error.code;
204
238
  return typeof code === "number" ? code : undefined;
205
239
  }
206
- function knownUnavailableReason(error) {
240
+ function knownUnavailableReason(error, beforePageSolve = false) {
207
241
  // Source-grounded mappings:
208
242
  // - apps/cdp-pool/src/index.ts: the JSON-RPC codes and messages below.
209
243
  // - src/runtime/browser.ts: BROWSER_CDP_POOL_REQUIRED and the two WebSocket messages.
@@ -230,7 +264,10 @@ function knownUnavailableReason(error) {
230
264
  error.message.includes("WebSocket closed")) {
231
265
  return "transport_failure";
232
266
  }
233
- return undefined;
267
+ // Browser creation, Playwright launch, and CDP connection all happen before the
268
+ // isolated-page handler is entered. Errors after that boundary belong to the
269
+ // challenge solve and must retain their existing classification.
270
+ return beforePageSolve ? "transport_failure" : undefined;
234
271
  }
235
272
  async function closeBrowserClient(client, timeoutMs, challengeKind, traceRecorder) {
236
273
  const close = client?.close;
@@ -274,10 +311,11 @@ export function createBrowserResolverVendorAdapter(options) {
274
311
  // Refuse so the chain can choose a vendor that can honor the resolved identity.
275
312
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
276
313
  }
314
+ const blockedRequests = new Set();
277
315
  const solveController = new AbortController();
278
316
  const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
279
317
  callerSignal.addEventListener("abort", onCallerAbort, { once: true });
280
- const timeout = setTimeout(() => solveController.abort(new BrowserSolveTimeoutError()), options.timeoutMs);
318
+ const timeout = setTimeout(() => solveController.abort(new BrowserSolveTimeoutError([...blockedRequests])), options.timeoutMs);
281
319
  let client;
282
320
  let handlerEntered = false;
283
321
  try {
@@ -290,7 +328,7 @@ export function createBrowserResolverVendorAdapter(options) {
290
328
  });
291
329
  const contextOperation = client.withIsolatedContext(async (page) => {
292
330
  handlerEntered = true;
293
- return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
331
+ return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, options.timeoutMs, blockedRequests, solveController.signal);
294
332
  });
295
333
  try {
296
334
  return await raceWithAbort(() => contextOperation, solveController.signal);
@@ -315,10 +353,15 @@ export function createBrowserResolverVendorAdapter(options) {
315
353
  cause: error,
316
354
  });
317
355
  }
356
+ if (error instanceof BrowserNavigationBlockedError) {
357
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "transport_failure", {
358
+ cause: error,
359
+ });
360
+ }
318
361
  if (error instanceof ResolverVendorUnavailableError) {
319
362
  throw error;
320
363
  }
321
- const reason = knownUnavailableReason(error);
364
+ const reason = knownUnavailableReason(error, !handlerEntered);
322
365
  if (reason) {
323
366
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, reason, { cause: error });
324
367
  }
@@ -1,4 +1,4 @@
1
- import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverVendor } from "../../types.js";
1
+ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor } from "../../types.js";
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"];
@@ -7,7 +7,15 @@ export declare const RESOLVER_VENDOR_CAPABILITIES: {
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"];
9
9
  };
10
+ /**
11
+ * SDK-owned fallback policy for hosted resolver vendors. Capability support is
12
+ * applied separately, so each provider receives only vendors that support one
13
+ * or more of its declared challenge kinds.
14
+ */
15
+ export declare const DEFAULT_RESOLVER_VENDOR_PREFERENCE: readonly ["capsolver", "2captcha"];
10
16
  export declare function resolverVendorSupports(vendor: ProviderResolverVendor, kind: ProviderChallengeKind): boolean;
17
+ /** Resolves an explicit provider override or the SDK-owned default vendor chain. */
18
+ export declare function resolveProviderResolverVendors(config: ProviderResolverConfig): readonly ProviderResolverVendor[];
11
19
  export interface ResolverIdentity {
12
20
  readonly proxyUrl: string;
13
21
  readonly userAgent: string;
@@ -33,9 +33,24 @@ export const RESOLVER_VENDOR_CAPABILITIES = {
33
33
  "akamai_sensor",
34
34
  ],
35
35
  };
36
+ /**
37
+ * SDK-owned fallback policy for hosted resolver vendors. Capability support is
38
+ * applied separately, so each provider receives only vendors that support one
39
+ * or more of its declared challenge kinds.
40
+ */
41
+ export const DEFAULT_RESOLVER_VENDOR_PREFERENCE = [
42
+ "capsolver",
43
+ "2captcha",
44
+ ];
36
45
  export function resolverVendorSupports(vendor, kind) {
37
46
  return RESOLVER_VENDOR_CAPABILITIES[vendor].includes(kind);
38
47
  }
48
+ /** Resolves an explicit provider override or the SDK-owned default vendor chain. */
49
+ export function resolveProviderResolverVendors(config) {
50
+ if (config.vendors !== undefined)
51
+ return config.vendors;
52
+ return DEFAULT_RESOLVER_VENDOR_PREFERENCE.filter((vendor) => config.kinds.some((kind) => resolverVendorSupports(vendor, kind)));
53
+ }
39
54
  export class ResolverVendorUnavailableError extends Error {
40
55
  vendor;
41
56
  reason;
@@ -4,6 +4,7 @@ import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorT
4
4
  import type { TraceRecorder } from "./trace.js";
5
5
  export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
6
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";
7
+ export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
7
8
  type EnvLike = Record<string, string | undefined>;
8
9
  type ResolverChainClient = ResolverContext & {
9
10
  solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
@@ -7,12 +7,13 @@ import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.j
7
7
  import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
8
8
  import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
9
9
  import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
10
- import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
10
+ import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
11
11
  import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
12
12
  import { 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";
13
13
  import { DEFAULT_PROFILE } from "./stealth.js";
14
14
  export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
15
15
  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";
16
+ export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
16
17
  const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
17
18
  const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
18
19
  const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
@@ -169,10 +170,12 @@ function assertKnownResolverVendor(vendor) {
169
170
  return;
170
171
  throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
171
172
  }
172
- function throwUnsupportedKind(kind) {
173
+ function throwUnsupportedKind(kind, usingDefaultVendors) {
173
174
  throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
174
175
  code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
175
- fix: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
176
+ fix: usingDefaultVendors
177
+ ? `No SDK default resolver vendor supports "${kind}". Declare an explicit resolver.vendors override with a supporting vendor.`
178
+ : `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
176
179
  });
177
180
  }
178
181
  function throwExhausted(attempts) {
@@ -542,8 +545,9 @@ function createResolverChainClient(options) {
542
545
  });
543
546
  }
544
547
  const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
545
- if (supportingEntries.length === 0)
546
- throwUnsupportedKind(challenge.kind);
548
+ if (supportingEntries.length === 0) {
549
+ throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
550
+ }
547
551
  signal.throwIfAborted();
548
552
  const identityResolution = options.proxyIntent
549
553
  ? await resolveResolverIdentity(options.proxyIntent)
@@ -692,7 +696,8 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
692
696
  return createUnsupportedResolverClient("Provider does not declare resolver capability");
693
697
  }
694
698
  assertClientProfileTransportContract(config.clientProfile, options.transport);
695
- if (config.vendors.length === 0) {
699
+ const vendors = resolveProviderResolverVendors(config);
700
+ if (config.vendors !== undefined && config.vendors.length === 0) {
696
701
  return createResolverChainClient({
697
702
  kinds: config.kinds,
698
703
  entries: [],
@@ -704,7 +709,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
704
709
  const allowedHosts = [...(options.allowedHosts ?? [])];
705
710
  return createResolverChainClient({
706
711
  kinds: config.kinds,
707
- entries: config.vendors.map((configuredVendor) => {
712
+ entries: vendors.map((configuredVendor) => {
708
713
  assertKnownResolverVendor(configuredVendor);
709
714
  const vendor = configuredVendor;
710
715
  return {
@@ -713,6 +718,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
713
718
  createAdapter: () => createAdapter(resolveVendorAvailability(vendor, env), timeoutMs, allowedHosts, adapterFactories),
714
719
  };
715
720
  }),
721
+ usingDefaultVendors: config.vendors === undefined,
716
722
  cache: options.cache,
717
723
  proxyIntent: options.proxyIntent,
718
724
  identityScope: options.identityScope,
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
4
  import { join } from "node:path";
@@ -924,6 +925,28 @@ function extractRequestId(raw) {
924
925
  const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
925
926
  return typeof value === "string" ? value : undefined;
926
927
  }
928
+ const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
929
+ function providerErrorCauseChain(error) {
930
+ if (!(error instanceof Error) && !isProviderError(error))
931
+ return undefined;
932
+ const seen = new Set([error]);
933
+ const frames = [];
934
+ let cause = error.cause;
935
+ while (frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
936
+ (cause instanceof Error || isProviderError(cause)) &&
937
+ !seen.has(cause)) {
938
+ seen.add(cause);
939
+ const message = cause.message;
940
+ frames.push({
941
+ errorClass: cause.name,
942
+ ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
943
+ messageLength: message.length,
944
+ messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
945
+ });
946
+ cause = cause.cause;
947
+ }
948
+ return frames.length > 0 ? frames : undefined;
949
+ }
927
950
  function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
928
951
  const code = isProviderError(error)
929
952
  ? (error.code ?? "provider_error")
@@ -934,6 +957,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
934
957
  : "internal_error";
935
958
  const errorClass = error instanceof Error ? error.name : typeof error;
936
959
  const message = error instanceof Error ? error.message : String(error);
960
+ const causeChain = providerErrorCauseChain(error);
937
961
  const details = errorObservabilityDetails(error, declaredErrorCode);
938
962
  const isUnregisteredProviderErrorCode = status === 500 &&
939
963
  isProviderError(error) &&
@@ -954,6 +978,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
954
978
  code,
955
979
  errorClass,
956
980
  message,
981
+ ...(causeChain ? { causeChain } : {}),
957
982
  ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
958
983
  errorCategory: details.category,
959
984
  taxonomyVersion: details.taxonomyVersion,
package/dist/types.d.ts CHANGED
@@ -252,16 +252,16 @@ export interface ProviderSttConfig {
252
252
  mode: ProviderSttMode;
253
253
  }
254
254
  /**
255
- * `browser` is the in-house CDP pool (`apps/cdp-pool`, reached through
256
- * `createBrowserClient`) and is a first-class vendor rather than an escape hatch:
257
- * for fingerprint-family kinds, it was measured faster than a paid vendor
258
- * (4.5 s vs 17.5 s) at zero marginal cost.
255
+ * Union order is documentation only.
259
256
  *
260
- * `2captcha` is the vendor already carrying production traffic in
261
- * `apifuse-provider-tabelog`.
257
+ * The SDK owns the default hosted-vendor fallback policy and derives the chain
258
+ * from each provider's declared challenge kinds. Hosted solvers are preferred
259
+ * with `capsolver` ahead of `2captcha` in that policy.
262
260
  *
263
- * Union order is documentation only; the effective fallback order is whatever
264
- * `ProviderResolverConfig.vendors` declares.
261
+ * `browser` is the in-house CDP pool and remains opt-in; it is excluded from the
262
+ * default chain. `custom` is likewise reserved for provider-specific configuration.
263
+ *
264
+ * `ProviderResolverConfig.vendors` overrides the SDK policy when declared.
265
265
  */
266
266
  export type ProviderResolverVendor = "browser" | "capsolver" | "capmonster" | "2captcha" | "custom";
267
267
  /**
@@ -339,8 +339,8 @@ export type ChallengeSolution = {
339
339
  readonly expires?: number;
340
340
  };
341
341
  export interface ProviderResolverConfig {
342
- /** Ordered vendor fallback chain, tried first to last. */
343
- readonly vendors: readonly ProviderResolverVendor[];
342
+ /** Optional ordered override for the SDK-owned vendor fallback chain. */
343
+ readonly vendors?: readonly ProviderResolverVendor[];
344
344
  /** Challenge kinds this provider is permitted to request. */
345
345
  readonly kinds: readonly ProviderChallengeKind[];
346
346
  /**
@@ -1355,8 +1355,6 @@ export interface NativeNetworkClient {
1355
1355
  export interface NativeContext {
1356
1356
  readonly network: NativeNetworkClient;
1357
1357
  }
1358
- /** Consumer-facing alias for the native capability on provider contexts. */
1359
- export type NativeProviderContext = NativeContext;
1360
1358
  export interface NativeProviderConfig {
1361
1359
  readonly network?: {
1362
1360
  readonly tcp?: readonly NativeTcpEgressRule[];
@@ -1496,6 +1494,10 @@ export interface BrowserPage extends BrowserFrame {
1496
1494
  cookies(): Promise<readonly BrowserCookie[]>;
1497
1495
  fill(selector: string, text: string): Promise<void>;
1498
1496
  goto(url: string): Promise<void>;
1497
+ goto(url: string, options?: {
1498
+ readonly timeout?: number;
1499
+ readonly waitUntil?: "load" | "domcontentloaded";
1500
+ }): Promise<void>;
1499
1501
  pageId?: string;
1500
1502
  screenshot(options?: {
1501
1503
  fullPage?: boolean;
@@ -1762,7 +1764,7 @@ export interface FlowContext {
1762
1764
  * un-keyed entries would be shared across all connectionless ceremonies. */
1763
1765
  readonly state?: ProviderRuntimeState;
1764
1766
  /** Present when the selected runtime supplies native network capabilities. */
1765
- readonly native?: NativeProviderContext;
1767
+ readonly native?: NativeContext;
1766
1768
  stealth: StealthClient;
1767
1769
  env: EnvContext;
1768
1770
  credential?: CredentialContext;
@@ -1863,8 +1865,8 @@ export interface ProviderContext {
1863
1865
  http: HttpClient;
1864
1866
  /** Present for requests carrying runtime-resolvable file references. */
1865
1867
  readonly files?: ProviderFilesContext;
1866
- /** Present when the selected runtime supplies native network capabilities. */
1867
- readonly native?: NativeProviderContext;
1868
+ /** Native network capability selected by declaration-derived contexts. */
1869
+ readonly native: NativeContext;
1868
1870
  cache: ProviderCache;
1869
1871
  state: ProviderRuntimeState;
1870
1872
  stealth: StealthClient;
@@ -1876,6 +1878,12 @@ export interface ProviderContext {
1876
1878
  resolver: ResolverContext;
1877
1879
  choice: ProviderChoiceContext;
1878
1880
  }
1881
+ /**
1882
+ * The operation context exposed for one provider declaration. Capability
1883
+ * bindings are present only when their corresponding declaration is present;
1884
+ * trace and request remain ambient runtime bindings.
1885
+ */
1886
+ export type ProviderContextFor<TConfig> = Pick<ProviderContext, "trace" | "request"> & ("env" extends keyof TConfig ? Pick<ProviderContext, "env"> : Record<never, never>) & ("credential" extends keyof TConfig ? Pick<ProviderContext, "credential"> : Record<never, never>) & ("http" extends keyof TConfig ? Pick<ProviderContext, "http"> : Record<never, never>) & ("files" extends keyof TConfig ? Pick<ProviderContext, "files"> : Record<never, never>) & ("native" extends keyof TConfig ? Pick<ProviderContext, "native"> : Record<never, never>) & ("cache" extends keyof TConfig ? Pick<ProviderContext, "cache"> : Record<never, never>) & ("state" extends keyof TConfig ? Pick<ProviderContext, "state"> : Record<never, never>) & ("stealth" extends keyof TConfig ? Pick<ProviderContext, "stealth"> : Record<never, never>) & ("browser" extends keyof TConfig ? Pick<ProviderContext, "browser"> : Record<never, never>) & ("auth" extends keyof TConfig ? Pick<ProviderContext, "auth"> : Record<never, never>) & ("ocr" extends keyof TConfig ? Pick<ProviderContext, "ocr"> : Record<never, never>) & ("stt" extends keyof TConfig ? Pick<ProviderContext, "stt"> : Record<never, never>) & ("resolver" extends keyof TConfig ? Pick<ProviderContext, "resolver"> : Record<never, never>) & ("choice" extends keyof TConfig ? Pick<ProviderContext, "choice"> : Record<never, never>);
1879
1887
  export interface ProxiedOAuthConfig {
1880
1888
  authorizeUrl: string;
1881
1889
  tokenUrl: string;
@@ -1921,7 +1929,7 @@ export interface OperationContractMetadata {
1921
1929
  lifecycle?: OperationLifecycle;
1922
1930
  deprecation?: OperationDeprecationMetadata;
1923
1931
  }
1924
- export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike> {
1932
+ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike, TContext = ProviderContext> {
1925
1933
  /**
1926
1934
  * Short English display title for the operation. The SDK passes it through
1927
1935
  * verbatim; the APIFuse registry derives the operation's en locale title
@@ -1951,7 +1959,7 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
1951
1959
  retryOnAuthRefresh?: boolean;
1952
1960
  input: TInput;
1953
1961
  output: TOutput;
1954
- handler(ctx: ProviderContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
1962
+ handler(ctx: TContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
1955
1963
  fixtures?: {
1956
1964
  request: InferSchemaOutput<TInput>;
1957
1965
  response: InferSchemaOutput<TOutput>;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.36",
2
+ "version": "2.2.0-beta.37",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -1,9 +1,9 @@
1
- import { defineProvider } from "@apifuse/provider-sdk/provider";
1
+ import { defineProvider, type ProviderContextOf } from "@apifuse/provider-sdk/provider";
2
2
 
3
3
  import { providerMeta } from "./meta";
4
4
  import { operations } from "./operations";
5
5
 
6
- export default defineProvider({
6
+ const buildProvider = defineProvider({
7
7
  id: "{{PROVIDER_ID}}",
8
8
  version: "1.0.0",
9
9
  runtime: "{{RUNTIME}}"{{BROWSER_BLOCK}},
@@ -11,5 +11,8 @@ export default defineProvider({
11
11
  reviewed: "community",
12
12
  {{SECRETS_BLOCK}}{{CREDENTIAL_BLOCK}}auth: {{AUTH_BLOCK}},
13
13
  meta: providerMeta,
14
- operations: operations,
15
14
  });
15
+
16
+ export type ProviderContext = ProviderContextOf<typeof buildProvider>;
17
+
18
+ export default buildProvider({ operations });
@@ -1,8 +1,9 @@
1
1
  import { defineOperation } from "@apifuse/provider-sdk/provider";
2
+ import type { ProviderContext } from "../index";
2
3
 
3
4
  import { pingInputSchema, pingOutputSchema } from "../schemas/ping";
4
5
 
5
- export const pingOperation = defineOperation({
6
+ export const pingOperation = defineOperation<ProviderContext>()({
6
7
  descriptionKey: "operations.ping.description",
7
8
  input: pingInputSchema,
8
9
  output: pingOutputSchema,