@apifuse/provider-sdk 2.2.0-beta.35 → 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 (44) hide show
  1. package/AUTHORING.md +22 -8
  2. package/CHANGELOG.md +8 -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/ceremonies/index.d.ts +8 -0
  9. package/dist/ceremonies/index.js +32 -25
  10. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  11. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  12. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  13. package/dist/define.d.ts +46 -25
  14. package/dist/define.js +381 -9
  15. package/dist/index.d.ts +2 -2
  16. package/dist/provider.d.ts +2 -1
  17. package/dist/runtime/browser.js +19 -11
  18. package/dist/runtime/choice.js +24 -7
  19. package/dist/runtime/resolver-public.d.ts +1 -1
  20. package/dist/runtime/resolver-public.js +1 -1
  21. package/dist/runtime/resolver-vendors/browser.js +57 -14
  22. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  23. package/dist/runtime/resolver-vendors/types.js +15 -0
  24. package/dist/runtime/resolver.d.ts +1 -0
  25. package/dist/runtime/resolver.js +13 -7
  26. package/dist/server/serve-implementation.js +25 -0
  27. package/dist/types.d.ts +25 -17
  28. package/package.json +6 -1
  29. package/src/ceremonies/index.ts +45 -31
  30. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  31. package/src/cli/templates/provider/index.ts.tpl +6 -3
  32. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  33. package/src/define.ts +462 -59
  34. package/src/index.ts +5 -2
  35. package/src/provider.ts +6 -1
  36. package/src/runtime/browser.ts +34 -11
  37. package/src/runtime/choice.ts +25 -8
  38. package/src/runtime/resolver-public.ts +2 -0
  39. package/src/runtime/resolver-vendors/browser.ts +69 -11
  40. package/src/runtime/resolver-vendors/types.ts +21 -0
  41. package/src/runtime/resolver.ts +17 -5
  42. package/src/server/serve-implementation.ts +39 -1
  43. package/src/testing/run.ts +3 -3
  44. package/src/types.ts +41 -17
@@ -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.35",
2
+ "version": "2.2.0-beta.37",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -111,6 +111,9 @@
111
111
  "type-check": "tsc --noEmit",
112
112
  "test": "bun test",
113
113
  "check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run lint:test-typesafety && bun run build",
114
+ "api:update": "bun run build && bun scripts/api-reports.ts update",
115
+ "api:check": "bun run build && bun scripts/api-reports.ts check",
116
+ "changeset:check": "bun scripts/check-changeset.ts",
114
117
  "pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
115
118
  "pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
116
119
  "pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
@@ -121,6 +124,8 @@
121
124
  "devDependencies": {
122
125
  "@arethetypeswrong/cli": "^0.18.5",
123
126
  "@biomejs/biome": "^2.5.0",
127
+ "@changesets/cli": "^3.0.1",
128
+ "@microsoft/api-extractor": "^7.58.13",
124
129
  "@types/bun": "latest",
125
130
  "@types/node": "^25.9.3",
126
131
  "ajv": "^8.17",
@@ -7,6 +7,7 @@ import {
7
7
  TurnValidationError,
8
8
  ValidationError,
9
9
  } from "../errors.js";
10
+ import type { AuthStartNoInputGuard } from "../define.js";
10
11
  import type { AuthFlowDefinition, AuthFlowInputHandler, AuthTurn, FlowContext } from "../types.js";
11
12
 
12
13
  type TurnKind = KnownAuthTurnKind;
@@ -224,6 +225,18 @@ export function validateCeremonyOutput(turn: unknown): AuthTurn {
224
225
  return turn as AuthTurn;
225
226
  }
226
227
 
228
+ /**
229
+ * Defines an auth flow while preserving its concrete type for downstream checks.
230
+ * The compile-time guard validates the inferred literal; annotating or widening
231
+ * a flow to `AuthFlowDefinition` before passing it defeats the check. Full
232
+ * enforcement requires branding, which is deferred to a future major.
233
+ */
234
+ export function defineAuthFlow<const TFlow extends AuthFlowDefinition>(
235
+ flow: TFlow & AuthStartNoInputGuard<TFlow>,
236
+ ): TFlow {
237
+ return flow;
238
+ }
239
+
227
240
  export function createOAuth2Ceremony(options: {
228
241
  authorizeUrl: string;
229
242
  tokenUrl: string;
@@ -232,7 +245,7 @@ export function createOAuth2Ceremony(options: {
232
245
  scopes: string[];
233
246
  usePKCE?: boolean;
234
247
  }): AuthFlowDefinition {
235
- return {
248
+ return defineAuthFlow({
236
249
  start: (ctx) =>
237
250
  runCeremonyHandler(
238
251
  async () => {
@@ -306,7 +319,7 @@ export function createOAuth2Ceremony(options: {
306
319
  input,
307
320
  ),
308
321
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "OAuth flow aborted." })),
309
- };
322
+ });
310
323
  }
311
324
 
312
325
  /**
@@ -384,7 +397,7 @@ export function createDeviceFlowCeremony(options: {
384
397
  clientSecretEnvKey?: string;
385
398
  scopes: string[];
386
399
  }): AuthFlowDefinition {
387
- return {
400
+ return defineAuthFlow({
388
401
  start: (ctx) =>
389
402
  runCeremonyHandler(
390
403
  async () => {
@@ -461,7 +474,7 @@ export function createDeviceFlowCeremony(options: {
461
474
  ),
462
475
  abort: async () =>
463
476
  validateCeremonyOutput(createTurn("abort", { hint: "Device flow aborted." })),
464
- };
477
+ });
465
478
  }
466
479
 
467
480
  export function createWebAuthnCeremony(options: {
@@ -470,7 +483,7 @@ export function createWebAuthnCeremony(options: {
470
483
  verifyUrl?: string;
471
484
  timeoutMs?: number;
472
485
  }): AuthFlowDefinition {
473
- return {
486
+ return defineAuthFlow({
474
487
  start: (ctx) =>
475
488
  runCeremonyHandler(
476
489
  async () => {
@@ -529,7 +542,7 @@ export function createWebAuthnCeremony(options: {
529
542
  ),
530
543
  abort: async () =>
531
544
  validateCeremonyOutput(createTurn("abort", { hint: "WebAuthn ceremony aborted." })),
532
- };
545
+ });
533
546
  }
534
547
 
535
548
  export function createMagicLinkCeremony(options: {
@@ -539,23 +552,31 @@ export function createMagicLinkCeremony(options: {
539
552
  expiresInMs?: number;
540
553
  }): AuthFlowDefinition {
541
554
  const emailField = options.emailField ?? "email";
555
+ const buildEmailForm = () =>
556
+ buildJsonSchemaForm(
557
+ {
558
+ type: "object",
559
+ required: [emailField],
560
+ properties: {
561
+ [emailField]: { type: "string", format: "email" },
562
+ },
563
+ },
564
+ "Provide the email address to receive a magic link.",
565
+ );
542
566
 
543
- return {
544
- start: (ctx, input = {}) =>
567
+ return defineAuthFlow({
568
+ start: (ctx) =>
569
+ runCeremonyHandler(
570
+ async () => buildEmailForm(),
571
+ "Magic link start failed",
572
+ ctx,
573
+ ),
574
+ continue: (ctx, input = {}) =>
545
575
  runCeremonyHandler(
546
576
  async () => {
547
577
  const email = getString(input, emailField);
548
578
  if (!email) {
549
- return buildJsonSchemaForm(
550
- {
551
- type: "object",
552
- required: [emailField],
553
- properties: {
554
- [emailField]: { type: "string", format: "email" },
555
- },
556
- },
557
- "Provide the email address to receive a magic link.",
558
- );
579
+ return buildEmailForm();
559
580
  }
560
581
 
561
582
  await ctx.http.post(options.sendUrl, { email });
@@ -570,17 +591,10 @@ export function createMagicLinkCeremony(options: {
570
591
  timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
571
592
  });
572
593
  },
573
- "Magic link start failed",
594
+ "Magic link continuation failed",
574
595
  ctx,
575
596
  input,
576
597
  ),
577
- continue: async () =>
578
- validateCeremonyOutput(
579
- createTurn("poll", {
580
- hint: "Continue polling for magic link completion.",
581
- timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
582
- }),
583
- ),
584
598
  poll: (ctx) =>
585
599
  runCeremonyHandler(
586
600
  async () => {
@@ -616,7 +630,7 @@ export function createMagicLinkCeremony(options: {
616
630
  ),
617
631
  abort: async () =>
618
632
  validateCeremonyOutput(createTurn("abort", { hint: "Magic link flow aborted." })),
619
- };
633
+ });
620
634
  }
621
635
 
622
636
  export function createFormCeremony(options: {
@@ -624,7 +638,7 @@ export function createFormCeremony(options: {
624
638
  hint?: string;
625
639
  mapCredential?: (input: Record<string, unknown>) => JsonObject;
626
640
  }): AuthFlowDefinition {
627
- return {
641
+ return defineAuthFlow({
628
642
  start: async () =>
629
643
  validateCeremonyOutput(
630
644
  buildJsonSchemaForm(
@@ -656,7 +670,7 @@ export function createFormCeremony(options: {
656
670
  ),
657
671
  abort: async () =>
658
672
  validateCeremonyOutput(createTurn("abort", { hint: "Form ceremony aborted." })),
659
- };
673
+ });
660
674
  }
661
675
 
662
676
  export function combineCeremonies(...ceremonies: AuthFlowDefinition[]): AuthFlowDefinition {
@@ -739,7 +753,7 @@ export function createSwitchCeremony(options: {
739
753
  }): AuthFlowDefinition {
740
754
  const choiceKeys = Object.keys(options.choices);
741
755
 
742
- return {
756
+ return defineAuthFlow({
743
757
  start: async () =>
744
758
  validateCeremonyOutput(
745
759
  createTurn("multi_choice", {
@@ -811,5 +825,5 @@ export function createSwitchCeremony(options: {
811
825
  "Switch ceremony abort failed",
812
826
  ctx,
813
827
  ),
814
- };
828
+ });
815
829
  }
@@ -1,6 +1,6 @@
1
1
  FROM oven/bun:1.2-alpine
2
2
  WORKDIR /provider
3
- COPY package.json bun.lockb* ./
3
+ COPY package.json bun.lock ./
4
4
  RUN bun install --frozen-lockfile
5
5
  COPY . .
6
6
  EXPOSE 3000