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

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 (47) 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/cli/templates/provider/index.ts.tpl +6 -3
  9. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  10. package/dist/declaration-validation.d.ts +2 -0
  11. package/dist/declaration-validation.js +29 -3
  12. package/dist/define.d.ts +39 -21
  13. package/dist/define.js +33 -9
  14. package/dist/health-scenario.d.ts +1842 -0
  15. package/dist/health-scenario.js +624 -0
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.js +1 -0
  18. package/dist/provider.d.ts +5 -1
  19. package/dist/provider.js +1 -0
  20. package/dist/runtime/browser.js +19 -11
  21. package/dist/runtime/resolver-public.d.ts +1 -1
  22. package/dist/runtime/resolver-public.js +1 -1
  23. package/dist/runtime/resolver-vendors/browser.js +57 -14
  24. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  25. package/dist/runtime/resolver-vendors/types.js +15 -0
  26. package/dist/runtime/resolver.d.ts +1 -0
  27. package/dist/runtime/resolver.js +13 -7
  28. package/dist/server/serve-implementation.d.ts +1 -1
  29. package/dist/server/serve-implementation.js +25 -0
  30. package/dist/server/types.d.ts +4 -4
  31. package/dist/types.d.ts +35 -25
  32. package/package.json +1 -1
  33. package/src/cli/templates/provider/index.ts.tpl +6 -3
  34. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  35. package/src/declaration-validation.ts +30 -3
  36. package/src/define.ts +144 -51
  37. package/src/health-scenario.ts +875 -0
  38. package/src/index.ts +78 -2
  39. package/src/provider.ts +81 -1
  40. package/src/runtime/browser.ts +34 -11
  41. package/src/runtime/resolver-public.ts +2 -0
  42. package/src/runtime/resolver-vendors/browser.ts +69 -11
  43. package/src/runtime/resolver-vendors/types.ts +21 -0
  44. package/src/runtime/resolver.ts +17 -5
  45. package/src/server/serve-implementation.ts +39 -1
  46. package/src/testing/run.ts +3 -3
  47. package/src/types.ts +52 -25
package/src/index.ts CHANGED
@@ -34,8 +34,84 @@ 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";
41
+ export {
42
+ AssertionExpressionSchema,
43
+ AssertionPredicateSchema,
44
+ AssertStepSchema,
45
+ BoundedJsonPathSchema,
46
+ CandidateBlockSchema,
47
+ CandidatePolicySchema,
48
+ CredentialRefDeclarationSchema,
49
+ defineHealthScenario,
50
+ HealthScenarioSchema,
51
+ HealthStepSchema,
52
+ ExtractStepSchema,
53
+ FindFirstSchema,
54
+ GuardStepSchema,
55
+ JournalPolicySchema,
56
+ JsonTemplateSchema,
57
+ ManualTriggerPolicySchema,
58
+ OperandSchema,
59
+ OperationStepSchema,
60
+ QuantifierSchema,
61
+ ReferenceSchema,
62
+ RetryPolicySchema,
63
+ SafeRegexSchema,
64
+ ScopedAssertionExpressionSchema,
65
+ ScopedAssertionPredicateSchema,
66
+ ScopedItemReferenceSchema,
67
+ ScopedOperandSchema,
68
+ StepReferenceSchema,
69
+ RelativeDateNodeSchema,
70
+ ValueTypeSchema,
71
+ } from "./health-scenario.js";
72
+ export type {
73
+ AssertionExpression,
74
+ AssertionPredicate,
75
+ AttemptReference,
76
+ AssertResult,
77
+ AssertStep,
78
+ BoundedJsonPath,
79
+ CandidateBlock,
80
+ CandidatePolicy,
81
+ CandidateReference,
82
+ CredentialReference,
83
+ CredentialRefDeclaration,
84
+ EstablishedConnectionReference,
85
+ ExtractStep,
86
+ FindFirst,
87
+ GuardAttribution,
88
+ GuardReasonCode,
89
+ GuardResult,
90
+ GuardStep,
91
+ HealthScenario,
92
+ HealthStep,
93
+ JsonTemplate,
94
+ JournalPolicy,
95
+ ManualTriggerPolicy,
96
+ NonEmpty,
97
+ OperationResult,
98
+ OperationStep,
99
+ Operand,
100
+ Quantifier,
101
+ ReferenceNode,
102
+ Reference,
103
+ RelativeDateNode,
104
+ RetryPolicy,
105
+ SafeRegex,
106
+ ScopedAssertionExpression,
107
+ ScopedAssertionPredicate,
108
+ ScopedItemReference,
109
+ ScopedOperand,
110
+ StepBase,
111
+ StepReference,
112
+ ExtractResult,
113
+ ValueType,
114
+ } from "./health-scenario.js";
39
115
  export type { DevServerOptions } from "./dev.js";
40
116
  export { createDevServer, startDevServer } from "./dev.js";
41
117
  export * from "./errors.js";
@@ -244,7 +320,6 @@ export type {
244
320
  NativeNetworkDynamicGrantOptions,
245
321
  NativeNetworkEgressGrant,
246
322
  NativeProviderConfig,
247
- NativeProviderContext,
248
323
  NativeProxyDrainHandler,
249
324
  NativeProxyEgressInfo,
250
325
  NativeProxyExpiringEvent,
@@ -301,6 +376,7 @@ export type {
301
376
  ProviderChallenge,
302
377
  ProviderChallengeKind,
303
378
  ProviderContext,
379
+ ProviderContextFor,
304
380
  ProviderDefinition,
305
381
  ProviderDeploymentOverrides,
306
382
  ProviderFileRef,
package/src/provider.ts CHANGED
@@ -35,6 +35,86 @@ 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";
43
+ export type { JsonPrimitive, JsonValue } from "./contract-json.js";
44
+ export {
45
+ AssertionExpressionSchema,
46
+ AssertionPredicateSchema,
47
+ AssertStepSchema,
48
+ BoundedJsonPathSchema,
49
+ CandidateBlockSchema,
50
+ CandidatePolicySchema,
51
+ CredentialRefDeclarationSchema,
52
+ defineHealthScenario,
53
+ HealthScenarioSchema,
54
+ HealthStepSchema,
55
+ ExtractStepSchema,
56
+ FindFirstSchema,
57
+ GuardStepSchema,
58
+ JournalPolicySchema,
59
+ JsonTemplateSchema,
60
+ ManualTriggerPolicySchema,
61
+ OperandSchema,
62
+ OperationStepSchema,
63
+ QuantifierSchema,
64
+ ReferenceSchema,
65
+ RetryPolicySchema,
66
+ SafeRegexSchema,
67
+ ScopedAssertionExpressionSchema,
68
+ ScopedAssertionPredicateSchema,
69
+ ScopedItemReferenceSchema,
70
+ ScopedOperandSchema,
71
+ StepReferenceSchema,
72
+ RelativeDateNodeSchema,
73
+ ValueTypeSchema,
74
+ } from "./health-scenario.js";
75
+ export type {
76
+ AssertionExpression,
77
+ AssertionPredicate,
78
+ AttemptReference,
79
+ AssertResult,
80
+ AssertStep,
81
+ BoundedJsonPath,
82
+ CandidateBlock,
83
+ CandidatePolicy,
84
+ CandidateReference,
85
+ CredentialReference,
86
+ CredentialRefDeclaration,
87
+ EstablishedConnectionReference,
88
+ ExtractStep,
89
+ FindFirst,
90
+ GuardAttribution,
91
+ GuardReasonCode,
92
+ GuardResult,
93
+ GuardStep,
94
+ HealthScenario,
95
+ HealthStep,
96
+ JsonTemplate,
97
+ JournalPolicy,
98
+ ManualTriggerPolicy,
99
+ NonEmpty,
100
+ OperationResult,
101
+ OperationStep,
102
+ Operand,
103
+ Quantifier,
104
+ ReferenceNode,
105
+ Reference,
106
+ RelativeDateNode,
107
+ RetryPolicy,
108
+ SafeRegex,
109
+ ScopedAssertionExpression,
110
+ ScopedAssertionPredicate,
111
+ ScopedItemReference,
112
+ ScopedOperand,
113
+ StepBase,
114
+ StepReference,
115
+ ExtractResult,
116
+ ValueType,
117
+ } from "./health-scenario.js";
38
118
  export {
39
119
  AuthError,
40
120
  HttpRedirectError,
@@ -107,7 +187,6 @@ export type {
107
187
  NativeNetworkDynamicGrantOptions,
108
188
  NativeNetworkEgressGrant,
109
189
  NativeProviderConfig,
110
- NativeProviderContext,
111
190
  NativeProxyDrainHandler,
112
191
  NativeProxyEgressInfo,
113
192
  NativeProxyExpiringEvent,
@@ -142,6 +221,7 @@ export type {
142
221
  ProviderChoiceIssueOptions,
143
222
  ProviderChoiceParseOptions,
144
223
  ProviderContext,
224
+ ProviderContextFor,
145
225
  ProviderDefinition,
146
226
  ProviderDeploymentOverrides,
147
227
  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;
@@ -35,6 +35,7 @@ import {
35
35
  type ResolverVendorTransport,
36
36
  ResolverVendorUnavailableError,
37
37
  type ResolverVendorUnavailableReason,
38
+ resolveProviderResolverVendors,
38
39
  resolverVendorSupports,
39
40
  } from "./resolver-vendors/types.js";
40
41
  import {
@@ -64,6 +65,10 @@ export {
64
65
  APIFUSE__RESOLVER__TIMEOUT_MS,
65
66
  DEFAULT_RESOLVER_TIMEOUT_MS,
66
67
  } from "./resolver-config.js";
68
+ export {
69
+ DEFAULT_RESOLVER_VENDOR_PREFERENCE,
70
+ resolveProviderResolverVendors,
71
+ } from "./resolver-vendors/types.js";
67
72
 
68
73
  type EnvLike = Record<string, string | undefined>;
69
74
 
@@ -340,10 +345,12 @@ function assertKnownResolverVendor(vendor: string): asserts vendor is ProviderRe
340
345
  throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
341
346
  }
342
347
 
343
- function throwUnsupportedKind(kind: ProviderChallengeKind): never {
348
+ function throwUnsupportedKind(kind: ProviderChallengeKind, usingDefaultVendors: boolean): never {
344
349
  throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
345
350
  code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
346
- fix: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
351
+ fix: usingDefaultVendors
352
+ ? `No SDK default resolver vendor supports "${kind}". Declare an explicit resolver.vendors override with a supporting vendor.`
353
+ : `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
347
354
  });
348
355
  }
349
356
 
@@ -814,6 +821,7 @@ function createResolverChainClient(options: {
814
821
  readonly kinds: readonly ProviderChallengeKind[];
815
822
  readonly entries: readonly ResolverChainEntry[];
816
823
  readonly unavailableReason?: string;
824
+ readonly usingDefaultVendors?: boolean;
817
825
  readonly cache?: ProviderCache;
818
826
  readonly identity?: ResolverIdentity;
819
827
  readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
@@ -839,7 +847,9 @@ function createResolverChainClient(options: {
839
847
  }
840
848
 
841
849
  const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
842
- if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
850
+ if (supportingEntries.length === 0) {
851
+ throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
852
+ }
843
853
  signal.throwIfAborted();
844
854
  const identityResolution = options.proxyIntent
845
855
  ? await resolveResolverIdentity(options.proxyIntent)
@@ -1033,7 +1043,8 @@ function createResolverClientFromEnvInternal(
1033
1043
  }
1034
1044
  assertClientProfileTransportContract(config.clientProfile, options.transport);
1035
1045
 
1036
- if (config.vendors.length === 0) {
1046
+ const vendors = resolveProviderResolverVendors(config);
1047
+ if (config.vendors !== undefined && config.vendors.length === 0) {
1037
1048
  return createResolverChainClient({
1038
1049
  kinds: config.kinds,
1039
1050
  entries: [],
@@ -1047,7 +1058,7 @@ function createResolverClientFromEnvInternal(
1047
1058
 
1048
1059
  return createResolverChainClient({
1049
1060
  kinds: config.kinds,
1050
- entries: config.vendors.map((configuredVendor) => {
1061
+ entries: vendors.map((configuredVendor) => {
1051
1062
  assertKnownResolverVendor(configuredVendor);
1052
1063
  const vendor = configuredVendor;
1053
1064
  return {
@@ -1062,6 +1073,7 @@ function createResolverClientFromEnvInternal(
1062
1073
  ),
1063
1074
  };
1064
1075
  }),
1076
+ usingDefaultVendors: config.vendors === undefined,
1065
1077
  cache: options.cache,
1066
1078
  proxyIntent: options.proxyIntent,
1067
1079
  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";
@@ -745,7 +746,7 @@ function createProviderContext(
745
746
  ...event,
746
747
  }),
747
748
  }),
748
- });
749
+ } as ProviderContext);
749
750
  wrappedContext = context;
750
751
  return context;
751
752
  }
@@ -1441,6 +1442,41 @@ function extractRequestId(raw: unknown): string | undefined {
1441
1442
  return typeof value === "string" ? value : undefined;
1442
1443
  }
1443
1444
 
1445
+ type ProviderErrorCauseFrame = {
1446
+ errorClass: string;
1447
+ code?: string;
1448
+ messageLength: number;
1449
+ messageFingerprint: string;
1450
+ };
1451
+
1452
+ const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
1453
+
1454
+ function providerErrorCauseChain(error: unknown): ProviderErrorCauseFrame[] | undefined {
1455
+ if (!(error instanceof Error) && !isProviderError(error)) return undefined;
1456
+
1457
+ const seen = new Set<object>([error]);
1458
+ const frames: ProviderErrorCauseFrame[] = [];
1459
+ let cause = error.cause;
1460
+
1461
+ while (
1462
+ frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
1463
+ (cause instanceof Error || isProviderError(cause)) &&
1464
+ !seen.has(cause)
1465
+ ) {
1466
+ seen.add(cause);
1467
+ const message = cause.message;
1468
+ frames.push({
1469
+ errorClass: cause.name,
1470
+ ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
1471
+ messageLength: message.length,
1472
+ messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
1473
+ });
1474
+ cause = cause.cause;
1475
+ }
1476
+
1477
+ return frames.length > 0 ? frames : undefined;
1478
+ }
1479
+
1444
1480
  function logProviderError(
1445
1481
  logger: ProviderServerLogger | unknown,
1446
1482
  provider: ProviderDefinition,
@@ -1461,6 +1497,7 @@ function logProviderError(
1461
1497
  : "internal_error";
1462
1498
  const errorClass = error instanceof Error ? error.name : typeof error;
1463
1499
  const message = error instanceof Error ? error.message : String(error);
1500
+ const causeChain = providerErrorCauseChain(error);
1464
1501
  const details = errorObservabilityDetails(error, declaredErrorCode);
1465
1502
  const isUnregisteredProviderErrorCode =
1466
1503
  status === 500 &&
@@ -1482,6 +1519,7 @@ function logProviderError(
1482
1519
  code,
1483
1520
  errorClass,
1484
1521
  message,
1522
+ ...(causeChain ? { causeChain } : {}),
1485
1523
  ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
1486
1524
  errorCategory: details.category,
1487
1525
  taxonomyVersion: details.taxonomyVersion,
@@ -525,7 +525,7 @@ function createUpstreamContext(
525
525
  ...(provider.native
526
526
  ? {
527
527
  native: {
528
- network: {
528
+ network: {
529
529
  connectTcp: async (options) => {
530
530
  const request = snapshotNativeConnectInput(options);
531
531
  requireNativeEgress().assertConnect(request, "disabled");
@@ -578,7 +578,7 @@ function createUpstreamContext(
578
578
  credential,
579
579
  state,
580
580
  }),
581
- };
581
+ } as ProviderContext;
582
582
  }
583
583
 
584
584
  function createNativeConnection(
@@ -709,7 +709,7 @@ export function createSnapshotContext(rawFixture: unknown): ProviderContext {
709
709
  credential,
710
710
  state,
711
711
  }),
712
- };
712
+ } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
713
713
  snapshotCaptureStates.set(context, {
714
714
  assertConsumed() {
715
715
  if (streamCaptureGroup && nextCaptureItem !== streamCaptureGroup.items.length) {