@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
package/src/define.ts CHANGED
@@ -31,6 +31,8 @@ import type {
31
31
  ProviderAccessConfig,
32
32
  ProviderChallengeKind,
33
33
  ProviderDefinition,
34
+ ProviderContext,
35
+ ProviderContextFor,
34
36
  ProviderOcrConfig,
35
37
  ProviderDeploymentOverrides,
36
38
  ProviderHealthMonitorConfig,
@@ -197,54 +199,65 @@ function parsePositiveMsDuration(value: string): number | undefined {
197
199
  return parsed;
198
200
  }
199
201
 
200
- type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
201
- type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
202
- OperationDefinition<TInput, TOutput>,
203
- "handler"
204
- > & {
202
+ type ProviderOperation = OperationDefinition<any, any, any>;
203
+ type OperationConfig<
204
+ TInput extends SchemaLike,
205
+ TOutput extends SchemaLike,
206
+ TContext = ProviderContext,
207
+ > = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
205
208
  handler(
206
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
209
+ ctx: TContext,
207
210
  input: InferSchemaOutput<TInput>,
208
211
  ):
209
212
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
210
213
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
211
214
  };
212
- type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
215
+ type OperationMapConfig<
216
+ TOperations extends Record<string, ProviderOperation>,
217
+ TContext = ProviderContext,
218
+ > = {
213
219
  [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
214
- ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
220
+ ? OperationConfig<TInput, TOutput, TContext> | OperationDefinition<TInput, TOutput, TContext>
215
221
  : never;
216
222
  };
217
- type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
218
- | SseOperationConfig<TInput, TOutput>
219
- | HttpStreamOperationConfig<TInput, TOutput>
220
- | WebSocketOperationConfig<TInput, TOutput>;
221
- type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
222
- OperationConfig<TInput, TOutput>,
223
- "handler" | "transport"
224
- > & {
223
+ type StreamOperationConfig<
224
+ TInput extends SchemaLike,
225
+ TOutput extends SchemaLike,
226
+ TContext = ProviderContext,
227
+ > =
228
+ | SseOperationConfig<TInput, TOutput, TContext>
229
+ | HttpStreamOperationConfig<TInput, TOutput, TContext>
230
+ | WebSocketOperationConfig<TInput, TOutput, TContext>;
231
+ type SseOperationConfig<
232
+ TInput extends SchemaLike,
233
+ TOutput extends SchemaLike,
234
+ TContext = ProviderContext,
235
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
225
236
  transport: OperationSseTransport;
226
237
  handler(
227
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
238
+ ctx: TContext,
228
239
  input: InferSchemaOutput<TInput>,
229
240
  ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
230
241
  };
231
- type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
232
- OperationConfig<TInput, TOutput>,
233
- "handler" | "transport"
234
- > & {
242
+ type HttpStreamOperationConfig<
243
+ TInput extends SchemaLike,
244
+ TOutput extends SchemaLike,
245
+ TContext = ProviderContext,
246
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
235
247
  transport: OperationHttpStreamTransport;
236
248
  handler(
237
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
249
+ ctx: TContext,
238
250
  input: InferSchemaOutput<TInput>,
239
251
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
240
252
  };
241
- type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
242
- OperationConfig<TInput, TOutput>,
243
- "handler" | "transport"
244
- > & {
253
+ type WebSocketOperationConfig<
254
+ TInput extends SchemaLike,
255
+ TOutput extends SchemaLike,
256
+ TContext = ProviderContext,
257
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
245
258
  transport: OperationWebSocketTransport;
246
259
  handler(
247
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
260
+ ctx: TContext,
248
261
  input: InferSchemaOutput<TInput>,
249
262
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
250
263
  };
@@ -562,7 +575,7 @@ function authStartHasHiddenInput(start: unknown): boolean {
562
575
  return /\s=\s/.test(second);
563
576
  }
564
577
 
565
- export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
578
+ export interface ProviderDeclaration {
566
579
  id: string;
567
580
  version: string;
568
581
  runtime: "standard" | "shared" | "browser";
@@ -573,6 +586,8 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
573
586
  * resolves omitted fields against the runtime deployment profiles.
574
587
  */
575
588
  deployment?: ProviderDeploymentOverrides;
589
+ /** Declares that provider operations use the SDK HTTP client. */
590
+ http?: true;
576
591
  allowedHosts?: string[];
577
592
  native?: NativeProviderConfig;
578
593
  stealth?: {
@@ -585,11 +600,21 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
585
600
  resolver?: ProviderResolverConfig;
586
601
  browser?: { engine: BrowserEngine };
587
602
  auth?: AuthConfig;
603
+ /** Declares that provider operations issue and consume SDK choice tokens. */
604
+ choice?: true;
588
605
  reviewed?: ProviderReviewed;
589
606
  access?: ProviderAccessConfig;
590
607
  secrets?: ProviderSecretDeclaration[];
608
+ /** Declares that provider operations read SDK-managed environment values. */
609
+ env?: true;
591
610
  credential?: CredentialDeclaration;
592
611
  context?: ContextDeclaration;
612
+ /** Declares that provider operations use SDK-managed persistent state. */
613
+ state?: true;
614
+ /** Declares that provider operations use the SDK provider cache. */
615
+ cache?: true;
616
+ /** Declares that provider operations access runtime-resolvable files. */
617
+ files?: true;
593
618
  meta: {
594
619
  displayName: string;
595
620
  displayNameKey?: string;
@@ -611,25 +636,35 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
611
636
  publicSchemaFieldNames?: "normalized";
612
637
  };
613
638
  };
614
- operations: OperationMapConfig<TOperations>;
615
639
  healthMonitor?: ProviderHealthMonitorConfig;
616
640
  /** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
617
641
  healthProbe?: ProviderHealthMonitorConfig;
618
642
  healthJourneys?: readonly HealthJourneyDefinition[];
619
643
  }
620
644
 
621
- /** Define one provider operation with schema-driven handler inference. */
622
- export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
623
- operation: OperationConfig<TInput, TOutput>,
624
- ): OperationDefinition<TInput, TOutput> {
625
- return operation;
645
+ interface ProviderConfig<
646
+ TOperations extends Record<string, ProviderOperation>,
647
+ TContext = ProviderContext,
648
+ > extends ProviderDeclaration {
649
+ operations: OperationMapConfig<TOperations, TContext>;
650
+ }
651
+
652
+ /** Define one factored provider operation with schema-driven handler inference. */
653
+ export function defineOperation<TContext>() {
654
+ return function operation<TInput extends SchemaLike, TOutput extends SchemaLike>(
655
+ config: OperationConfig<TInput, TOutput, TContext>,
656
+ ): OperationDefinition<TInput, TOutput, TContext> {
657
+ return config;
658
+ };
626
659
  }
627
660
 
628
- /** Define a non-JSON provider operation with explicit transport metadata. */
629
- export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
630
- operation: StreamOperationConfig<TInput, TOutput>,
631
- ): OperationDefinition<TInput, TOutput> {
632
- return operation;
661
+ /** Define a factored non-JSON operation with explicit transport metadata. */
662
+ export function defineStreamOperation<TContext>() {
663
+ return function streamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
664
+ config: StreamOperationConfig<TInput, TOutput, TContext>,
665
+ ): OperationDefinition<TInput, TOutput, TContext> {
666
+ return config;
667
+ };
633
668
  }
634
669
 
635
670
  function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
@@ -780,6 +815,14 @@ function validateProviderShape(config: unknown): void {
780
815
  assertRequiredField(config, "operations", String(config.id));
781
816
  if (typeof config.runtime === "string")
782
817
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
818
+ if (config.native !== undefined && config.runtime === "browser") {
819
+ throw new ValidationError(
820
+ `Provider "${String(config.id)}" cannot declare capability "native" with runtime "browser"`,
821
+ {
822
+ fix: 'Use runtime: "standard" or runtime: "shared", or remove the native declaration.',
823
+ },
824
+ );
825
+ }
783
826
  const auth = config.auth;
784
827
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
785
828
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
@@ -1087,12 +1130,14 @@ function validateProviderResolver(config: { id: string; resolver?: ProviderResol
1087
1130
  "resolver",
1088
1131
  config.id,
1089
1132
  );
1090
- validateResolverLiteralArray(
1091
- resolver.vendors,
1092
- "resolver.vendors",
1093
- VALID_PROVIDER_RESOLVER_VENDORS,
1094
- config.id,
1095
- );
1133
+ if (resolver.vendors !== undefined) {
1134
+ validateResolverLiteralArray(
1135
+ resolver.vendors,
1136
+ "resolver.vendors",
1137
+ VALID_PROVIDER_RESOLVER_VENDORS,
1138
+ config.id,
1139
+ );
1140
+ }
1096
1141
  validateResolverLiteralArray(
1097
1142
  resolver.kinds,
1098
1143
  "resolver.kinds",
@@ -2783,12 +2828,48 @@ function validateProviderDeployment(providerId: string, deployment: unknown): vo
2783
2828
  });
2784
2829
  }
2785
2830
 
2786
- export function defineProvider<
2831
+ /** The second authoring phase for a declaration established by defineProvider. */
2832
+ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <
2833
+ TOperations extends Record<string, ProviderOperation>,
2834
+ >(
2835
+ implementation: {
2836
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2837
+ },
2838
+ ) => ProviderDefinition & {
2839
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2840
+ };
2841
+
2842
+ /** Extract the declaration-derived operation context from a provider builder. */
2843
+ export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration>
2844
+ ? ProviderContextFor<TDeclaration>
2845
+ : never;
2846
+
2847
+ /** Establish a provider declaration before its operations are contextually typed. */
2848
+ export function defineProvider<const TDeclaration extends ProviderDeclaration>(
2849
+ declaration: TDeclaration &
2850
+ Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> &
2851
+ AuthStartNoInputGuard<TDeclaration>,
2852
+ ): ProviderBuilder<TDeclaration> {
2853
+ const buildProvider = <TOperations extends Record<string, ProviderOperation>>(
2854
+ implementation: {
2855
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2856
+ },
2857
+ ) =>
2858
+ finalizeProvider({
2859
+ ...declaration,
2860
+ ...implementation,
2861
+ } as ProviderConfig<TOperations, ProviderContextFor<TDeclaration>>);
2862
+ return buildProvider as ProviderBuilder<TDeclaration>;
2863
+ }
2864
+
2865
+ function finalizeProvider<
2787
2866
  TOperations extends Record<string, ProviderOperation>,
2788
- TConfig extends ProviderConfig<TOperations>,
2867
+ TContext,
2789
2868
  >(
2790
- config: TConfig & AuthStartNoInputGuard<TConfig>,
2791
- ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2869
+ config: ProviderConfig<TOperations, TContext>,
2870
+ ): ProviderDefinition & {
2871
+ operations: OperationMapConfig<TOperations, TContext>;
2872
+ } {
2792
2873
  validateProviderShape(config);
2793
2874
  const operations = resolveOperationFixtureRequests(config.operations);
2794
2875
  if (!CONNECTOR_ID_REGEX.test(config.id))
@@ -2849,7 +2930,9 @@ export function defineProvider<
2849
2930
  `Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
2850
2931
  { fix: 'Set runtime: "browser" or remove the browser config' },
2851
2932
  );
2852
- const provider: ProviderDefinition & { operations: OperationMapConfig<TOperations> } = {
2933
+ const provider: ProviderDefinition & {
2934
+ operations: OperationMapConfig<TOperations, TContext>;
2935
+ } = {
2853
2936
  id: config.id,
2854
2937
  version: config.version,
2855
2938
  runtime: config.runtime,
@@ -2871,7 +2954,8 @@ export function defineProvider<
2871
2954
  credential: config.credential,
2872
2955
  context: config.context,
2873
2956
  meta: config.meta,
2874
- operations,
2957
+ operations: operations as ProviderDefinition["operations"] &
2958
+ OperationMapConfig<TOperations, TContext>,
2875
2959
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2876
2960
  // was declared onto both so old and new consumers keep working.
2877
2961
  healthMonitor: config.healthMonitor ?? config.healthProbe,
package/src/index.ts CHANGED
@@ -34,7 +34,9 @@ export {
34
34
  defineStreamOperation,
35
35
  every,
36
36
  type AuthStartNoInputGuard,
37
- type ProviderConfig,
37
+ type ProviderBuilder,
38
+ type ProviderContextOf,
39
+ type ProviderDeclaration,
38
40
  } from "./define.js";
39
41
  export type { DevServerOptions } from "./dev.js";
40
42
  export { createDevServer, startDevServer } from "./dev.js";
@@ -244,7 +246,6 @@ export type {
244
246
  NativeNetworkDynamicGrantOptions,
245
247
  NativeNetworkEgressGrant,
246
248
  NativeProviderConfig,
247
- NativeProviderContext,
248
249
  NativeProxyDrainHandler,
249
250
  NativeProxyEgressInfo,
250
251
  NativeProxyExpiringEvent,
@@ -301,6 +302,7 @@ export type {
301
302
  ProviderChallenge,
302
303
  ProviderChallengeKind,
303
304
  ProviderContext,
305
+ ProviderContextFor,
304
306
  ProviderDefinition,
305
307
  ProviderDeploymentOverrides,
306
308
  ProviderFileRef,
package/src/provider.ts CHANGED
@@ -35,6 +35,11 @@ export {
35
35
  defineSmsOtpMatcher,
36
36
  every,
37
37
  } from "./define.js";
38
+ export type {
39
+ ProviderBuilder,
40
+ ProviderContextOf,
41
+ ProviderDeclaration,
42
+ } from "./define.js";
38
43
  export {
39
44
  AuthError,
40
45
  HttpRedirectError,
@@ -107,7 +112,6 @@ export type {
107
112
  NativeNetworkDynamicGrantOptions,
108
113
  NativeNetworkEgressGrant,
109
114
  NativeProviderConfig,
110
- NativeProviderContext,
111
115
  NativeProxyDrainHandler,
112
116
  NativeProxyEgressInfo,
113
117
  NativeProxyExpiringEvent,
@@ -142,6 +146,7 @@ export type {
142
146
  ProviderChoiceIssueOptions,
143
147
  ProviderChoiceParseOptions,
144
148
  ProviderContext,
149
+ ProviderContextFor,
145
150
  ProviderDefinition,
146
151
  ProviderDeploymentOverrides,
147
152
  ProviderFileRef,
@@ -804,8 +804,11 @@ class PlaywrightBrowserPage implements BrowserPageContract {
804
804
  private readonly proxy: LaunchOptions["proxy"] = undefined,
805
805
  ) {}
806
806
 
807
- async goto(url: string): Promise<void> {
808
- await this.page.goto(url);
807
+ async goto(
808
+ url: string,
809
+ options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
810
+ ): Promise<void> {
811
+ await this.page.goto(url, options);
809
812
  }
810
813
 
811
814
  async evaluate<T>(fn: string | (() => T)): Promise<T> {
@@ -1492,17 +1495,33 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1492
1495
  return this.pageId;
1493
1496
  }
1494
1497
 
1495
- async goto(url: string): Promise<void> {
1498
+ async goto(
1499
+ url: string,
1500
+ options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
1501
+ ): Promise<void> {
1496
1502
  await this.initialize();
1497
1503
  const startedAt = Date.now();
1498
- let loadEventSeen = false;
1499
- const unsubscribe = this.pageClient.on("Page.loadEventFired", () => {
1500
- loadEventSeen = true;
1504
+ const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
1505
+ const waitUntil = options?.waitUntil ?? "load";
1506
+ let expectedEventSeen = false;
1507
+ const eventName =
1508
+ waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
1509
+ const unsubscribe = this.pageClient.on(eventName, () => {
1510
+ expectedEventSeen = true;
1501
1511
  });
1502
1512
 
1503
1513
  try {
1504
- await this.pageClient.send("Page.navigate", { url });
1505
- await this.waitForDocumentReady(startedAt + DEFAULT_WAIT_TIMEOUT_MS, () => loadEventSeen);
1514
+ const navigation = (await this.pageClient.send("Page.navigate", { url })) as {
1515
+ readonly errorText?: unknown;
1516
+ };
1517
+ if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
1518
+ throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
1519
+ }
1520
+ await this.waitForDocumentReady(
1521
+ startedAt + timeout,
1522
+ () => expectedEventSeen,
1523
+ waitUntil,
1524
+ );
1506
1525
  } finally {
1507
1526
  unsubscribe();
1508
1527
  }
@@ -1866,12 +1885,16 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1866
1885
 
1867
1886
  private async waitForDocumentReady(
1868
1887
  deadline: number,
1869
- isLoadEventSeen: () => boolean,
1888
+ isExpectedEventSeen: () => boolean,
1889
+ waitUntil: "load" | "domcontentloaded",
1870
1890
  ): Promise<void> {
1871
1891
  while (Date.now() < deadline) {
1872
1892
  const readyState = await this.evaluate<string>("document.readyState");
1873
- if (readyState === "complete" || readyState === "interactive") {
1874
- if (isLoadEventSeen() || readyState === "complete") {
1893
+ const documentReady =
1894
+ readyState === "complete" ||
1895
+ (waitUntil === "domcontentloaded" && readyState === "interactive");
1896
+ if (documentReady) {
1897
+ if (isExpectedEventSeen() || readyState === "complete") {
1875
1898
  return;
1876
1899
  }
1877
1900
  }
@@ -8,10 +8,12 @@ export {
8
8
  createResolverClient,
9
9
  createResolverClientFromEnv,
10
10
  createUnsupportedResolverClient,
11
+ DEFAULT_RESOLVER_VENDOR_PREFERENCE,
11
12
  DEFAULT_RESOLVER_TIMEOUT_MS,
12
13
  invalidateResolverSolution,
13
14
  RESOLVER_ADAPTER_REGISTRY,
14
15
  RESOLVER_INSTRUMENTATION_METADATA,
16
+ resolveProviderResolverVendors,
15
17
  type ResolverAdapterFactory,
16
18
  type ResolverInstrumentationMetadata,
17
19
  type ResolverRuntimeOptions,
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  const BROWSER_VENDOR_ID = "browser" as const;
20
20
  const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
21
+ const NAVIGATION_BLOCKED_ERROR_TEXT = "net::ERR_BLOCKED_BY_CLIENT";
21
22
  const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
22
23
  const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
23
24
 
@@ -65,12 +66,41 @@ export interface BrowserResolverVendorAdapter extends ResolverVendorAdapter {
65
66
  }
66
67
 
67
68
  class BrowserSolveTimeoutError extends Error {
68
- constructor() {
69
- super("Browser resolver solve budget elapsed");
69
+ constructor(blockedRequests: readonly string[]) {
70
+ super(
71
+ `Browser resolver solve budget elapsed${formatBlockedRequests(blockedRequests)}`,
72
+ );
70
73
  this.name = "BrowserSolveTimeoutError";
71
74
  }
72
75
  }
73
76
 
77
+ class BrowserNavigationBlockedError extends Error {
78
+ readonly code = "RESOLVER_BROWSER_NAVIGATION_BLOCKED";
79
+
80
+ constructor(
81
+ readonly navigationUrl: string,
82
+ readonly blockedUrls: readonly string[],
83
+ options: ErrorOptions,
84
+ ) {
85
+ super(
86
+ `Browser resolver navigation was blocked for ${navigationUrl}${formatBlockedRequests(blockedUrls)}`,
87
+ options,
88
+ );
89
+ this.name = "BrowserNavigationBlockedError";
90
+ }
91
+ }
92
+
93
+ function formatBlockedRequests(blockedRequests: readonly string[]): string {
94
+ if (blockedRequests.length === 0) return "";
95
+ const displayed = blockedRequests.slice(0, 5);
96
+ const remainder = blockedRequests.length - displayed.length;
97
+ return `; blocked ${blockedRequests.length} requests: [${displayed.join(", ")}]${remainder > 0 ? ` (+${remainder} more)` : ""}`;
98
+ }
99
+
100
+ function isNavigationBlockedError(error: unknown): error is Error {
101
+ return error instanceof Error && error.message.includes(NAVIGATION_BLOCKED_ERROR_TEXT);
102
+ }
103
+
74
104
  class BrowserCleanupTimeoutError extends Error {
75
105
  constructor(timeoutMs: number) {
76
106
  super(`Browser resolver cleanup exceeded ${timeoutMs}ms`);
@@ -223,6 +253,8 @@ async function solveInPage(
223
253
  allowedHosts: readonly string[],
224
254
  successCookieName: string,
225
255
  pollIntervalMs: number,
256
+ gotoTimeoutMs: number,
257
+ blockedRequests: Set<string>,
226
258
  signal: AbortSignal,
227
259
  ): Promise<Extract<ChallengeSolution, { readonly form: "cookies" }>> {
228
260
  return await page.withResourcePolicy(
@@ -232,11 +264,13 @@ async function solveInPage(
232
264
  routes: [
233
265
  {
234
266
  match: () => true,
235
- handle: (request) => ({
236
- action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
237
- ? "continue"
238
- : "block",
239
- }),
267
+ handle: (request) => {
268
+ if (isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)) {
269
+ return { action: "continue" };
270
+ }
271
+ blockedRequests.add(request.url);
272
+ return { action: "block" };
273
+ },
240
274
  },
241
275
  ],
242
276
  },
@@ -245,7 +279,19 @@ async function solveInPage(
245
279
  () => page.userAgent(),
246
280
  signal,
247
281
  );
248
- await raceWithAbort(() => page.goto(pageUrl), signal);
282
+ try {
283
+ await raceWithAbort(
284
+ () =>
285
+ page.goto(pageUrl, {
286
+ timeout: gotoTimeoutMs,
287
+ waitUntil: "domcontentloaded",
288
+ }),
289
+ signal,
290
+ );
291
+ } catch (error) {
292
+ if (!isNavigationBlockedError(error)) throw error;
293
+ throw new BrowserNavigationBlockedError(pageUrl, [...blockedRequests], { cause: error });
294
+ }
249
295
 
250
296
  while (true) {
251
297
  const cookies = await raceWithAbort(() => page.cookies(), signal);
@@ -304,6 +350,7 @@ function poolErrorCode(error: Error): number | undefined {
304
350
 
305
351
  function knownUnavailableReason(
306
352
  error: unknown,
353
+ beforePageSolve = false,
307
354
  ): "allocation_exhausted" | "missing_credentials" | "transport_failure" | undefined {
308
355
  // Source-grounded mappings:
309
356
  // - apps/cdp-pool/src/index.ts: the JSON-RPC codes and messages below.
@@ -338,7 +385,10 @@ function knownUnavailableReason(
338
385
  return "transport_failure";
339
386
  }
340
387
 
341
- return undefined;
388
+ // Browser creation, Playwright launch, and CDP connection all happen before the
389
+ // isolated-page handler is entered. Errors after that boundary belong to the
390
+ // challenge solve and must retain their existing classification.
391
+ return beforePageSolve ? "transport_failure" : undefined;
342
392
  }
343
393
 
344
394
  async function closeBrowserClient(
@@ -400,11 +450,12 @@ export function createBrowserResolverVendorAdapter(
400
450
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
401
451
  }
402
452
 
453
+ const blockedRequests = new Set<string>();
403
454
  const solveController = new AbortController();
404
455
  const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
405
456
  callerSignal.addEventListener("abort", onCallerAbort, { once: true });
406
457
  const timeout = setTimeout(
407
- () => solveController.abort(new BrowserSolveTimeoutError()),
458
+ () => solveController.abort(new BrowserSolveTimeoutError([...blockedRequests])),
408
459
  options.timeoutMs,
409
460
  );
410
461
 
@@ -427,6 +478,8 @@ export function createBrowserResolverVendorAdapter(
427
478
  options.allowedHosts,
428
479
  SUCCESS_COOKIE_NAMES[challengeKind],
429
480
  pollIntervalMs,
481
+ options.timeoutMs,
482
+ blockedRequests,
430
483
  solveController.signal,
431
484
  );
432
485
  });
@@ -457,10 +510,15 @@ export function createBrowserResolverVendorAdapter(
457
510
  cause: error,
458
511
  });
459
512
  }
513
+ if (error instanceof BrowserNavigationBlockedError) {
514
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "transport_failure", {
515
+ cause: error,
516
+ });
517
+ }
460
518
  if (error instanceof ResolverVendorUnavailableError) {
461
519
  throw error;
462
520
  }
463
- const reason = knownUnavailableReason(error);
521
+ const reason = knownUnavailableReason(error, !handlerEntered);
464
522
  if (reason) {
465
523
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, reason, { cause: error });
466
524
  }
@@ -2,6 +2,7 @@ import type {
2
2
  ChallengeSolution,
3
3
  ProviderChallenge,
4
4
  ProviderChallengeKind,
5
+ ProviderResolverConfig,
5
6
  ProviderResolverVendor,
6
7
  } from "../../types.js";
7
8
  import type { TraceRecorder } from "../trace.js";
@@ -42,6 +43,16 @@ export const RESOLVER_VENDOR_CAPABILITIES = {
42
43
  ],
43
44
  } as const satisfies Readonly<Record<ProviderResolverVendor, readonly ProviderChallengeKind[]>>;
44
45
 
46
+ /**
47
+ * SDK-owned fallback policy for hosted resolver vendors. Capability support is
48
+ * applied separately, so each provider receives only vendors that support one
49
+ * or more of its declared challenge kinds.
50
+ */
51
+ export const DEFAULT_RESOLVER_VENDOR_PREFERENCE = [
52
+ "capsolver",
53
+ "2captcha",
54
+ ] as const satisfies readonly ProviderResolverVendor[];
55
+
45
56
  export function resolverVendorSupports(
46
57
  vendor: ProviderResolverVendor,
47
58
  kind: ProviderChallengeKind,
@@ -49,6 +60,16 @@ export function resolverVendorSupports(
49
60
  return (RESOLVER_VENDOR_CAPABILITIES[vendor] as readonly ProviderChallengeKind[]).includes(kind);
50
61
  }
51
62
 
63
+ /** Resolves an explicit provider override or the SDK-owned default vendor chain. */
64
+ export function resolveProviderResolverVendors(
65
+ config: ProviderResolverConfig,
66
+ ): readonly ProviderResolverVendor[] {
67
+ if (config.vendors !== undefined) return config.vendors;
68
+ return DEFAULT_RESOLVER_VENDOR_PREFERENCE.filter((vendor) =>
69
+ config.kinds.some((kind) => resolverVendorSupports(vendor, kind)),
70
+ );
71
+ }
72
+
52
73
  export interface ResolverIdentity {
53
74
  readonly proxyUrl: string;
54
75
  readonly userAgent: string;