@apifuse/provider-sdk 2.2.0-beta.58 → 2.2.0-beta.60

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 (150) hide show
  1. package/AUTHORING.md +25 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +18 -2
  4. package/bin/apifuse-dev.ts +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-perf.ts +41 -4
  7. package/bin/apifuse-record.ts +9 -2
  8. package/dist/cli/templates/provider/README.md.tpl +15 -3
  9. package/dist/config/loader.d.ts +7 -0
  10. package/dist/config/loader.js +43 -18
  11. package/dist/declaration-validation.d.ts +2 -1
  12. package/dist/declaration-validation.js +39 -0
  13. package/dist/define.d.ts +4 -4
  14. package/dist/define.js +25 -1
  15. package/dist/engine.d.ts +16 -0
  16. package/dist/engine.js +59 -3
  17. package/dist/error-resolution.js +13 -0
  18. package/dist/index.d.ts +7 -4
  19. package/dist/index.js +4 -2
  20. package/dist/lint.js +38 -17
  21. package/dist/provider.d.ts +3 -3
  22. package/dist/provider.js +2 -2
  23. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  24. package/dist/runtime/bounded-expiring-map.js +64 -0
  25. package/dist/runtime/browser.js +2 -1
  26. package/dist/runtime/cache.d.ts +2 -0
  27. package/dist/runtime/cache.js +11 -3
  28. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  29. package/dist/runtime/diagnostic-env-values.js +57 -0
  30. package/dist/runtime/diagnostic-env.d.ts +11 -0
  31. package/dist/runtime/diagnostic-env.js +35 -0
  32. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  33. package/dist/runtime/diagnostic-redactor.js +569 -0
  34. package/dist/runtime/egress-lease.d.ts +65 -0
  35. package/dist/runtime/egress-lease.js +236 -0
  36. package/dist/runtime/env.js +2 -1
  37. package/dist/runtime/http.js +1 -0
  38. package/dist/runtime/instrumentation.js +3 -0
  39. package/dist/runtime/ocr.js +2 -1
  40. package/dist/runtime/otlp.js +5 -4
  41. package/dist/runtime/proxy-nodemaven.js +3 -2
  42. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  43. package/dist/runtime/proxy-retry-policy.js +11 -2
  44. package/dist/runtime/request-options.d.ts +7 -0
  45. package/dist/runtime/request-options.js +62 -22
  46. package/dist/runtime/request-telemetry.d.ts +7 -9
  47. package/dist/runtime/resolver-config.d.ts +7 -0
  48. package/dist/runtime/resolver-config.js +8 -0
  49. package/dist/runtime/resolver-public.d.ts +3 -1
  50. package/dist/runtime/resolver-public.js +1 -1
  51. package/dist/runtime/resolver-shared.d.ts +9 -0
  52. package/dist/runtime/resolver-shared.js +57 -0
  53. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  54. package/dist/runtime/resolver-telemetry.js +204 -0
  55. package/dist/runtime/resolver-usage.d.ts +24 -0
  56. package/dist/runtime/resolver-usage.js +60 -0
  57. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  58. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  59. package/dist/runtime/resolver-vendors/browser.js +0 -1
  60. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  61. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  62. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  63. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  64. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  65. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  66. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  67. package/dist/runtime/resolver-vendors/types.js +20 -10
  68. package/dist/runtime/resolver.d.ts +5 -1
  69. package/dist/runtime/resolver.js +400 -111
  70. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  71. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  72. package/dist/runtime/stealth.js +547 -53
  73. package/dist/runtime/stt.js +3 -2
  74. package/dist/runtime/trace.d.ts +5 -0
  75. package/dist/runtime/trace.js +115 -20
  76. package/dist/schema.d.ts +20 -0
  77. package/dist/schema.js +72 -2
  78. package/dist/server/index.d.ts +2 -0
  79. package/dist/server/index.js +1 -0
  80. package/dist/server/self-test-redaction.js +8 -33
  81. package/dist/server/self-test.d.ts +4 -0
  82. package/dist/server/self-test.js +10 -1
  83. package/dist/server/sensitive-values.d.ts +26 -0
  84. package/dist/server/sensitive-values.js +131 -0
  85. package/dist/server/serve-implementation.d.ts +7 -1
  86. package/dist/server/serve-implementation.js +394 -125
  87. package/dist/server/trace-output.d.ts +2 -1
  88. package/dist/server/trace-output.js +46 -14
  89. package/dist/server/types.d.ts +6 -0
  90. package/dist/server/types.js +14 -0
  91. package/dist/stealth/profiles.d.ts +7 -1
  92. package/dist/stealth/profiles.js +9 -0
  93. package/dist/testing/run.js +6 -0
  94. package/dist/trace-sanitization.d.ts +4 -3
  95. package/dist/trace-sanitization.js +45 -22
  96. package/dist/types.d.ts +106 -10
  97. package/package.json +2 -1
  98. package/src/cli/templates/provider/README.md.tpl +15 -3
  99. package/src/config/loader.ts +47 -18
  100. package/src/declaration-validation.ts +47 -1
  101. package/src/define.ts +45 -3
  102. package/src/engine.ts +78 -3
  103. package/src/error-resolution.ts +13 -0
  104. package/src/index.ts +34 -1
  105. package/src/lint.ts +44 -18
  106. package/src/provider.ts +10 -0
  107. package/src/runtime/bounded-expiring-map.ts +67 -0
  108. package/src/runtime/browser.ts +2 -1
  109. package/src/runtime/cache.ts +13 -3
  110. package/src/runtime/diagnostic-env-values.ts +56 -0
  111. package/src/runtime/diagnostic-env.ts +55 -0
  112. package/src/runtime/diagnostic-redactor.ts +662 -0
  113. package/src/runtime/egress-lease.ts +341 -0
  114. package/src/runtime/env.ts +2 -1
  115. package/src/runtime/http.ts +1 -0
  116. package/src/runtime/instrumentation.ts +3 -0
  117. package/src/runtime/ocr.ts +2 -1
  118. package/src/runtime/otlp.ts +5 -4
  119. package/src/runtime/proxy-nodemaven.ts +3 -2
  120. package/src/runtime/proxy-retry-policy.ts +12 -1
  121. package/src/runtime/request-options.ts +76 -28
  122. package/src/runtime/request-telemetry.ts +5 -7
  123. package/src/runtime/resolver-config.ts +12 -0
  124. package/src/runtime/resolver-public.ts +21 -0
  125. package/src/runtime/resolver-shared.ts +75 -0
  126. package/src/runtime/resolver-telemetry.ts +362 -0
  127. package/src/runtime/resolver-usage.ts +96 -0
  128. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  129. package/src/runtime/resolver-vendors/browser.ts +0 -1
  130. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  131. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  132. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  133. package/src/runtime/resolver-vendors/types.ts +60 -12
  134. package/src/runtime/resolver.ts +468 -126
  135. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  136. package/src/runtime/stealth.ts +727 -95
  137. package/src/runtime/stt.ts +3 -2
  138. package/src/runtime/trace.ts +168 -26
  139. package/src/schema.ts +80 -2
  140. package/src/server/index.ts +21 -0
  141. package/src/server/self-test-redaction.ts +10 -40
  142. package/src/server/self-test.ts +13 -0
  143. package/src/server/sensitive-values.ts +200 -0
  144. package/src/server/serve-implementation.ts +561 -136
  145. package/src/server/trace-output.ts +77 -17
  146. package/src/server/types.ts +14 -0
  147. package/src/stealth/profiles.ts +10 -0
  148. package/src/testing/run.ts +6 -0
  149. package/src/trace-sanitization.ts +70 -23
  150. package/src/types.ts +124 -19
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { createHash } from "node:crypto";
2
3
 
3
4
  import {
@@ -17,7 +18,9 @@ import type {
17
18
  ProviderResolverVendor,
18
19
  ResolverContext,
19
20
  } from "../types.js";
21
+ import { isProviderCacheBypassed } from "./cache.js";
20
22
  import {
23
+ RESOLVER_CHALLENGE_BINDINGS,
21
24
  resolverChallengeAllowsDirectCache,
22
25
  resolverChallengeIsCacheable,
23
26
  resolverChallengeIsIdentityScoped,
@@ -25,21 +28,26 @@ import {
25
28
  } from "./resolver-vendors/bindings.js";
26
29
  import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
27
30
  import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
31
+ import { createHypersolutionsResolverVendorAdapter } from "./resolver-vendors/hypersolutions.js";
28
32
  import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
29
33
  import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
30
34
  import {
31
35
  RESOLVER_VENDOR_CAPABILITIES,
32
36
  type ResolverIdentity,
33
37
  type ResolverIssuingIdentity,
38
+ type ResolverPaidUsageContext,
34
39
  type ResolverVendorAdapter,
35
40
  type ResolverVendorTransport,
36
41
  ResolverVendorUnavailableError,
42
+ attachResolverVendorPollObserver,
37
43
  type ResolverVendorUnavailableReason,
38
44
  resolveProviderResolverVendors,
39
45
  resolverVendorSupports,
40
46
  } from "./resolver-vendors/types.js";
41
47
  import {
42
48
  createUnsupportedResolverClient,
49
+ inheritResolverTelemetryBinding,
50
+ registerResolverTelemetryBinding,
43
51
  RESOLVER_INSTRUMENTATION_METADATA,
44
52
  type ResolverSolveWithRecorder,
45
53
  } from "./resolver-shared.js";
@@ -48,11 +56,18 @@ import {
48
56
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
49
57
  APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
50
58
  APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
59
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
51
60
  APIFUSE__RESOLVER__TIMEOUT_MS,
52
61
  DEFAULT_RESOLVER_TIMEOUT_MS,
62
+ kindRequiresClientProfile,
53
63
  } from "./resolver-config.js";
54
64
  import { DEFAULT_STEALTH_PROFILE } from "./stealth.js";
55
65
  import type { TraceRecorder } from "./trace.js";
66
+ import type {
67
+ ResolverTelemetryErrorClass,
68
+ ResolverTelemetryPhase,
69
+ ResolverTelemetrySink,
70
+ } from "./resolver-telemetry.js";
56
71
 
57
72
  export {
58
73
  createUnsupportedResolverClient,
@@ -63,6 +78,7 @@ export {
63
78
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
64
79
  APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
65
80
  APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
81
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
66
82
  APIFUSE__RESOLVER__TIMEOUT_MS,
67
83
  DEFAULT_RESOLVER_TIMEOUT_MS,
68
84
  } from "./resolver-config.js";
@@ -109,6 +125,8 @@ type ResolverChainClient = ResolverContext & {
109
125
  export interface ResolverRuntimeOptions {
110
126
  readonly allowedHosts?: readonly string[];
111
127
  readonly cache?: ProviderCache;
128
+ /** Optional request-scoped observability sink; never used as resolver identity. */
129
+ readonly telemetry?: ResolverTelemetrySink;
112
130
  /** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
113
131
  readonly proxyIntent?: {
114
132
  readonly mode: ProviderProxyMode;
@@ -231,6 +249,13 @@ const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAd
231
249
  timeoutMs,
232
250
  });
233
251
  },
252
+ hypersolutions(configuration, timeoutMs, allowedHosts) {
253
+ return createHypersolutionsResolverVendorAdapter({
254
+ allowedHosts,
255
+ apiKey: configuration,
256
+ timeoutMs,
257
+ });
258
+ },
234
259
  };
235
260
 
236
261
  export const RESOLVER_ADAPTER_REGISTRY: Partial<
@@ -285,12 +310,12 @@ type ResolverChainEntry = {
285
310
  };
286
311
 
287
312
  function normalizedEnvValue(env: EnvLike, key: string): string | undefined {
288
- const value = env[key]?.trim();
313
+ const value = readDiagnosticEnv(key, env)?.trim();
289
314
  return value ? value : undefined;
290
315
  }
291
316
 
292
317
  function readPositiveIntegerEnv(env: EnvLike, name: string): string | undefined {
293
- const raw = env[name]?.trim();
318
+ const raw = readDiagnosticEnv(name, env)?.trim();
294
319
  if (!raw) return undefined;
295
320
  if (!/^[1-9]\d*$/.test(raw)) {
296
321
  throw new Error(`${name} must be a positive integer`);
@@ -302,6 +327,12 @@ function assertDeclaredKind(
302
327
  requestedKind: ProviderChallengeKind,
303
328
  declaredKinds: readonly ProviderChallengeKind[],
304
329
  ): void {
330
+ if (!Object.hasOwn(RESOLVER_CHALLENGE_BINDINGS, requestedKind)) {
331
+ throw new ProviderError(`Unknown resolver kind "${requestedKind}"`, {
332
+ code: "RESOLVER_KIND_NOT_DECLARED",
333
+ fix: "Use a challenge kind exported by @apifuse/provider-sdk.",
334
+ });
335
+ }
305
336
  if (declaredKinds.includes(requestedKind)) return;
306
337
 
307
338
  const declared = declaredKinds.length > 0 ? declaredKinds.join(", ") : "none";
@@ -365,23 +396,26 @@ function throwUnsupportedKind(kind: ProviderChallengeKind, usingDefaultVendors:
365
396
  });
366
397
  }
367
398
 
368
- function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
399
+ function throwExhausted(
400
+ challengeKind: ProviderChallengeKind,
401
+ attempts: readonly ResolverChainAttempt[],
402
+ executedAttemptCount = attempts.length,
403
+ ): never {
369
404
  const hasMissingChallengeInput = attempts.some(
370
405
  (attempt) => attempt.reason === "missing_challenge_input",
371
406
  );
372
- const summary = attempts
373
- .map(({ vendor, reason, missingFields }) =>
374
- missingFields === undefined || missingFields.length === 0
375
- ? `${vendor}: ${reason}`
376
- : `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`,
377
- )
378
- .join(", ");
379
- throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
407
+ throw new ProviderError("The challenge could not be resolved.", {
380
408
  code: "RESOLVER_CHAIN_EXHAUSTED",
381
409
  fix: hasMissingChallengeInput
382
- ? "Capture the named challenge fields or configure another supporting resolver vendor."
383
- : "Configure another supporting resolver vendor or restore an unavailable vendor.",
384
- details: attempts,
410
+ ? "Capture all required challenge fields and retry the request."
411
+ : "Retry the request or contact support if the challenge continues.",
412
+ retryable: false,
413
+ details: {
414
+ challengeKind,
415
+ attempts: executedAttemptCount,
416
+ outcome: "exhausted",
417
+ retryable: false,
418
+ },
385
419
  });
386
420
  }
387
421
 
@@ -451,6 +485,16 @@ function restrictResolverTransport(
451
485
  allowedHosts: readonly string[],
452
486
  ): ResolverVendorTransport {
453
487
  return {
488
+ sessionHeaders: transport.sessionHeaders,
489
+ ...(transport.getCookie
490
+ ? {
491
+ getCookie(name, url) {
492
+ // Jar reads are gated like fetches: an adapter may only read state for declared hosts.
493
+ assertResolverHostAllowed(url, allowedHosts);
494
+ return transport.getCookie?.(name, url);
495
+ },
496
+ }
497
+ : {}),
454
498
  async fetch(url, init) {
455
499
  // Empty declarations remain deny-by-default, matching the adapter-factory/browser path.
456
500
  assertResolverHostAllowed(url, allowedHosts);
@@ -494,10 +538,21 @@ function safePhase(phase: string | undefined): string | undefined {
494
538
  return phase !== undefined && /^[a-z\d_.:-]{1,64}$/i.test(phase) ? phase : undefined;
495
539
  }
496
540
 
497
- function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChainAttempt {
498
- const cause = safeCause(error);
541
+ function unavailableAttempt(
542
+ error: ResolverVendorUnavailableError,
543
+ forTelemetry = false,
544
+ ): ResolverChainAttempt {
545
+ // Preserve the full input for the collector's redact-then-bound path. Span
546
+ // attributes retain their existing sanitization and phase validation.
547
+ const cause =
548
+ forTelemetry && error.cause !== undefined
549
+ ? {
550
+ name: error.cause instanceof Error ? error.cause.name : "Error",
551
+ message: error.cause instanceof Error ? error.cause.message : String(error.cause),
552
+ }
553
+ : safeCause(error);
499
554
  const upstreamHost = safeUpstreamHost(error.upstreamHost);
500
- const phase = safePhase(error.phase);
555
+ const phase = forTelemetry ? error.phase : safePhase(error.phase);
501
556
  const round =
502
557
  Number.isSafeInteger(error.round) && (error.round as number) > 0 ? error.round : undefined;
503
558
  return {
@@ -524,6 +579,125 @@ function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Recor
524
579
  };
525
580
  }
526
581
 
582
+ type ResolverAttemptDiagnostics = {
583
+ phase?: string;
584
+ pollCount?: number;
585
+ vendorErrorCode?: string;
586
+ vendorErrorDescription?: string;
587
+ };
588
+
589
+ function resolverTelemetryPhase(value: unknown): ResolverTelemetryPhase | undefined {
590
+ return value === "create_task" ||
591
+ value === "poll_result" ||
592
+ value === "cleanup" ||
593
+ value === "measure_ip" ||
594
+ value === "fetch_script" ||
595
+ value === "generate_payload" ||
596
+ value === "post_payload"
597
+ ? value
598
+ : undefined;
599
+ }
600
+
601
+ function diagnosticString(value: unknown): string | undefined {
602
+ return typeof value === "string" && value.length > 0 ? value : undefined;
603
+ }
604
+
605
+ function diagnosticCount(value: unknown): number | undefined {
606
+ return typeof value === "number" && Number.isFinite(value)
607
+ ? Math.max(0, Math.floor(value))
608
+ : undefined;
609
+ }
610
+
611
+ function createResolverTelemetryTraceRecorder(input: {
612
+ base?: TraceRecorder;
613
+ vendor: ProviderResolverVendor;
614
+ attemptIndex: number;
615
+ telemetry?: ResolverTelemetrySink;
616
+ diagnostics: ResolverAttemptDiagnostics;
617
+ }): TraceRecorder | undefined {
618
+ if (!input.base && !input.telemetry) return undefined;
619
+ const recorder: TraceRecorder = {
620
+ async runSpan<T>(
621
+ name: string,
622
+ fn: () => Promise<T> | T,
623
+ spanOptions: {
624
+ attributes?: Record<string, unknown>;
625
+ onSuccess?: (value: T) => Record<string, unknown> | undefined;
626
+ onError?: (error: unknown) => Record<string, unknown> | undefined;
627
+ } = {},
628
+ ) {
629
+ const adapterPhase = name.startsWith("resolver.vendor.")
630
+ ? diagnosticString(name.replace("resolver.vendor.", ""))
631
+ : undefined;
632
+ const phase = resolverTelemetryPhase(adapterPhase);
633
+ const startedAt = Date.now();
634
+ let attributes: Record<string, unknown> | undefined;
635
+ let spanOutcome: "ok" | "error" = "ok";
636
+ const wrappedOptions = {
637
+ ...spanOptions,
638
+ onSuccess(value: T) {
639
+ attributes = spanOptions.onSuccess?.(value);
640
+ return attributes;
641
+ },
642
+ onError(error: unknown) {
643
+ attributes = spanOptions.onError?.(error);
644
+ return attributes;
645
+ },
646
+ };
647
+ try {
648
+ if (input.base) return await input.base.runSpan(name, fn, wrappedOptions);
649
+ const value = await fn();
650
+ attributes = spanOptions.onSuccess?.(value);
651
+ return value;
652
+ } catch (error) {
653
+ spanOutcome = "error";
654
+ if (!input.base) attributes = spanOptions.onError?.(error);
655
+ throw error;
656
+ } finally {
657
+ if (adapterPhase && adapterPhase !== "attempt") {
658
+ const reportedPhase = diagnosticString(attributes?.transport_phase) ?? adapterPhase;
659
+ const pollCount = diagnosticCount(attributes?.poll_count);
660
+ const vendorErrorCode = diagnosticString(attributes?.vendor_error_code);
661
+ const vendorErrorDescription = diagnosticString(
662
+ attributes?.vendor_error_description ?? attributes?.error_message,
663
+ );
664
+ if (phase === "cleanup") {
665
+ input.telemetry?.recordVendorAttempt({
666
+ vendor: input.vendor,
667
+ phase: "cleanup",
668
+ diagnostics: { phase: reportedPhase, attemptIndex: input.attemptIndex },
669
+ outcome: spanOutcome,
670
+ ms: Date.now() - startedAt,
671
+ ...(pollCount === undefined ? {} : { pollCount }),
672
+ ...(vendorErrorCode ? { vendorErrorCode } : {}),
673
+ ...(vendorErrorDescription ? { vendorErrorDescription } : {}),
674
+ ...(spanOutcome === "error" ? { errorClass: "unexpected" as const } : {}),
675
+ });
676
+ } else {
677
+ input.diagnostics.phase = reportedPhase;
678
+ if (pollCount !== undefined) input.diagnostics.pollCount = pollCount;
679
+ if (vendorErrorCode) input.diagnostics.vendorErrorCode = vendorErrorCode;
680
+ if (vendorErrorDescription) {
681
+ input.diagnostics.vendorErrorDescription = vendorErrorDescription;
682
+ }
683
+ }
684
+ }
685
+ }
686
+ },
687
+ };
688
+ return attachResolverVendorPollObserver(recorder, () => {
689
+ input.diagnostics.pollCount = (input.diagnostics.pollCount ?? 0) + 1;
690
+ });
691
+ }
692
+
693
+ function resolverAttemptErrorClass(
694
+ error: unknown,
695
+ signal: AbortSignal,
696
+ ): ResolverTelemetryErrorClass {
697
+ if (signal.aborted) return "aborted";
698
+ return error instanceof ResolverVendorUnavailableError ? error.reason : "unexpected";
699
+ }
700
+
527
701
  function challengeOrigin(challenge: ProviderChallenge): string {
528
702
  return new URL(challenge.pageUrl).origin;
529
703
  }
@@ -710,13 +884,13 @@ async function cacheResolverSolution(
710
884
  solution: ChallengeSolution,
711
885
  identity: ResolverIssuingIdentity,
712
886
  identityScope: string | undefined,
713
- ): Promise<void> {
714
- if (!resolverChallengeIsCacheable(challenge)) return;
887
+ ): Promise<{ written: boolean; reason?: "no_expires" | "not_cacheable" }> {
888
+ if (!resolverChallengeIsCacheable(challenge)) return { written: false, reason: "not_cacheable" };
715
889
  const expiresAtMs = solutionExpiryMs(solution);
716
890
  const now = Date.now();
717
- if (expiresAtMs === undefined) return;
891
+ if (expiresAtMs === undefined) return { written: false, reason: "no_expires" };
718
892
  const ttlMs = Math.floor(expiresAtMs - now);
719
- if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS) return;
893
+ if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS) return { written: false, reason: "no_expires" };
720
894
 
721
895
  const scopedDigest =
722
896
  identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
@@ -727,7 +901,7 @@ async function cacheResolverSolution(
727
901
  scopedDigest === undefined &&
728
902
  identity.proxyUrl === undefined
729
903
  ) {
730
- return;
904
+ return { written: false, reason: "not_cacheable" };
731
905
  }
732
906
  const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
733
907
  await cache.set(
@@ -736,7 +910,7 @@ async function cacheResolverSolution(
736
910
  { ttlMs },
737
911
  );
738
912
  rememberSolutionIssuer(solution, issuerDigest);
739
- if (scopedDigest !== undefined || identity.proxyUrl !== undefined) return;
913
+ if (scopedDigest !== undefined || identity.proxyUrl !== undefined) return { written: true };
740
914
 
741
915
  const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
742
916
  const current = await cache.get(indexKey);
@@ -750,6 +924,7 @@ async function cacheResolverSolution(
750
924
  ],
751
925
  now,
752
926
  );
927
+ return { written: true };
753
928
  }
754
929
 
755
930
  async function invalidateResolverSolutionWithOutcome(
@@ -866,6 +1041,10 @@ async function resolveResolverIdentity(
866
1041
  }
867
1042
  }
868
1043
 
1044
+ function createChainTelemetryBinding(options: Parameters<typeof createResolverChainClient>[0]) {
1045
+ return (telemetry: ResolverTelemetrySink) => createResolverChainClient({ ...options, telemetry });
1046
+ }
1047
+
869
1048
  function createResolverChainClient(options: {
870
1049
  readonly kinds: readonly ProviderChallengeKind[];
871
1050
  readonly entries: readonly ResolverChainEntry[];
@@ -879,128 +1058,284 @@ function createResolverChainClient(options: {
879
1058
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
880
1059
  readonly clientProfile?: string;
881
1060
  readonly allowedHosts?: readonly string[];
1061
+ readonly telemetry?: ResolverTelemetrySink;
882
1062
  }): ResolverChainClient {
883
1063
  assertClientProfileTransportContract(options.clientProfile, options.transport);
1064
+ const cache =
1065
+ options.cache && !isProviderCacheBypassed(options.cache) ? options.cache : undefined;
884
1066
  const client: ResolverChainClient = {
885
1067
  async solve(
886
1068
  challenge: ProviderChallenge,
887
1069
  signal: AbortSignal = new AbortController().signal,
888
1070
  traceRecorder?: TraceRecorder,
889
1071
  ) {
890
- assertDeclaredKind(challenge.kind, options.kinds);
891
- if (options.unavailableReason) {
892
- throw new ProviderError(options.unavailableReason, {
893
- code: "RESOLVER_UNAVAILABLE",
894
- fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
895
- });
896
- }
1072
+ const solveStartedAt = Date.now();
1073
+ let terminalOutcome: "solved" | "cached" | "exhausted" | "aborted" | "error" = "error";
1074
+ try {
1075
+ assertDeclaredKind(challenge.kind, options.kinds);
1076
+ if (options.unavailableReason) {
1077
+ throw new ProviderError(options.unavailableReason, {
1078
+ code: "RESOLVER_UNAVAILABLE",
1079
+ fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
1080
+ });
1081
+ }
897
1082
 
898
- const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
899
- if (supportingEntries.length === 0) {
900
- throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
901
- }
902
- signal.throwIfAborted();
903
- const identityResolution = options.proxyIntent
904
- ? await resolveResolverIdentity(options.proxyIntent)
905
- : { identity: options.identity };
906
- const identity = identityResolution.identity;
907
- signal.throwIfAborted();
908
- // Resolve a required proxy lease before consulting the cache. Solutions minted
909
- // under a previous release are shared and long-lived, but a portable cached token
910
- // must not bypass the upstream admission policy when no lease can be resolved.
911
- const requiredProxyIdentityMissing =
912
- options.proxyIntent?.mode === "required" && identity === undefined;
913
- if (requiredProxyIdentityMissing) {
914
- throwExhausted(
915
- supportingEntries.map((entry) => ({
916
- vendor: entry.id,
917
- reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
918
- })),
919
- );
920
- }
921
- if (options.cache && resolverChallengeIsCacheable(challenge)) {
922
- const cached = await findCachedSolution(
923
- options.cache,
924
- challenge,
925
- identity,
926
- options.identityScope,
927
- );
928
- if (cached) return cached;
929
- }
930
- const attempts: ResolverChainAttempt[] = [];
931
- for (const entry of supportingEntries) {
932
- const adapter = entry.createAdapter();
933
- try {
934
- const solveAttempt = () => {
935
- const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
936
- const unrestrictedTransport =
937
- options.transport ??
938
- (requiresTransport
939
- ? options.createTransport?.({
940
- clientProfile: options.clientProfile,
941
- identityScope: options.identityScope,
942
- })
943
- : undefined);
944
- if (requiresTransport && unrestrictedTransport === undefined) {
945
- throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
1083
+ const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
1084
+ if (supportingEntries.length === 0) {
1085
+ throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
1086
+ }
1087
+ if (kindRequiresClientProfile(challenge.kind) && !options.clientProfile?.trim()) {
1088
+ options.telemetry?.recordIdentity({ source: "none", failure: "missing_client_profile" });
1089
+ throwExhausted(
1090
+ challenge.kind,
1091
+ supportingEntries.map((entry) => ({
1092
+ vendor: entry.id,
1093
+ reason: "missing_client_profile",
1094
+ })),
1095
+ 0,
1096
+ );
1097
+ }
1098
+ signal.throwIfAborted();
1099
+ const identityResolution = options.proxyIntent
1100
+ ? await resolveResolverIdentity(options.proxyIntent)
1101
+ : { identity: options.identity };
1102
+ const identity = identityResolution.identity;
1103
+ options.telemetry?.recordIdentity({
1104
+ // For SBSD, "declared" identifies the configured client profile and its bound
1105
+ // session identity; the engine independently supplies solver credentials.
1106
+ source:
1107
+ kindRequiresClientProfile(challenge.kind) && options.clientProfile
1108
+ ? "declared"
1109
+ : options.proxyIntent
1110
+ ? (identityResolution.userAgentSource ?? "defaulted")
1111
+ : identity
1112
+ ? "declared"
1113
+ : "none",
1114
+ ...(identityResolution.unavailableReason
1115
+ ? { failure: identityResolution.unavailableReason }
1116
+ : {}),
1117
+ });
1118
+ signal.throwIfAborted();
1119
+ const requiredProxyIdentityMissing =
1120
+ options.proxyIntent?.mode === "required" && identity === undefined;
1121
+ if (requiredProxyIdentityMissing) {
1122
+ const reason = identityResolution.unavailableReason ?? "missing_proxy_identity";
1123
+ for (const [entryIndex, entry] of supportingEntries.entries()) {
1124
+ options.telemetry?.recordVendorAttempt({
1125
+ vendor: entry.id,
1126
+ phase: "create_task",
1127
+ diagnostics: { attemptIndex: entryIndex + 1 },
1128
+ outcome: "error",
1129
+ ms: 0,
1130
+ errorClass: reason,
1131
+ });
1132
+ const next = supportingEntries[entryIndex + 1];
1133
+ if (next) {
1134
+ options.telemetry?.recordFailover({ from: entry.id, to: next.id, reason });
946
1135
  }
947
- const transport = unrestrictedTransport
948
- ? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
949
- : undefined;
950
- return adapter.solve(challenge, identity, signal, traceRecorder, transport);
951
- };
952
- const solution = traceRecorder
953
- ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
954
- attributes: {
955
- vendor: adapter.id,
956
- challenge_kind: challenge.kind,
957
- client_profile: options.clientProfile,
958
- resolver_identity_source: identityResolution.userAgentSource,
959
- },
960
- onError(error) {
961
- return error instanceof ResolverVendorUnavailableError
962
- ? unavailableSpanAttributes(error)
963
- : undefined;
964
- },
965
- })
966
- : await solveAttempt();
967
- if (
968
- options.cache &&
969
- resolverChallengeIsCacheable(challenge) &&
970
- solution.form === "cookies" &&
971
- solutionExpiryMs(solution) !== undefined
972
- ) {
973
- const issuingIdentity =
974
- adapter.getIssuingIdentity?.(solution, identity, challenge) ??
975
- resolverChallengeIssuingIdentity(challenge, {
976
- ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
977
- userAgent: solution.userAgent,
978
- });
979
- if (issuingIdentity) {
980
- await cacheResolverSolution(
981
- options.cache,
1136
+ }
1137
+ throwExhausted(
1138
+ challenge.kind,
1139
+ supportingEntries.map((entry) => ({
1140
+ vendor: entry.id,
1141
+ reason,
1142
+ })),
1143
+ );
1144
+ }
1145
+ if (!cache) {
1146
+ options.telemetry?.recordCacheRead({ status: "disabled", challengeKind: challenge.kind });
1147
+ } else if (!resolverChallengeIsCacheable(challenge)) {
1148
+ options.telemetry?.recordCacheRead({
1149
+ status: "not_cacheable",
1150
+ challengeKind: challenge.kind,
1151
+ });
1152
+ } else {
1153
+ const cached = await findCachedSolution(
1154
+ cache,
1155
+ challenge,
1156
+ identity,
1157
+ options.identityScope,
1158
+ );
1159
+ options.telemetry?.recordCacheRead({
1160
+ status: cached ? "hit" : "miss",
1161
+ challengeKind: challenge.kind,
1162
+ });
1163
+ if (cached) {
1164
+ terminalOutcome = "cached";
1165
+ return cached;
1166
+ }
1167
+ }
1168
+ const attempts: ResolverChainAttempt[] = [];
1169
+ for (const [entryIndex, entry] of supportingEntries.entries()) {
1170
+ const adapter = entry.createAdapter();
1171
+ const attemptStartedAt = Date.now();
1172
+ const diagnostics: ResolverAttemptDiagnostics = {};
1173
+ const telemetryTraceRecorder = createResolverTelemetryTraceRecorder({
1174
+ base: traceRecorder,
1175
+ vendor: adapter.id,
1176
+ attemptIndex: entryIndex + 1,
1177
+ telemetry: options.telemetry,
1178
+ diagnostics,
1179
+ });
1180
+ let solution: ChallengeSolution;
1181
+ try {
1182
+ const solveAttempt = () => {
1183
+ const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
1184
+ const unrestrictedTransport =
1185
+ options.transport ??
1186
+ (requiresTransport
1187
+ ? options.createTransport?.({
1188
+ clientProfile: options.clientProfile,
1189
+ identityScope: options.identityScope,
1190
+ })
1191
+ : undefined);
1192
+ if (requiresTransport && unrestrictedTransport === undefined) {
1193
+ throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
1194
+ }
1195
+ const transport = unrestrictedTransport
1196
+ ? restrictResolverTransport(unrestrictedTransport, [
1197
+ ...(options.allowedHosts ?? []),
1198
+ ...(adapter.transportAllowedHosts ?? []),
1199
+ ])
1200
+ : undefined;
1201
+ const usage: ResolverPaidUsageContext = {
1202
+ // Position in the declared chain, not in the kind-filtered list.
1203
+ vendorIndex: options.entries.indexOf(entry) + 1,
1204
+ ...(options.identityScope
1205
+ ? {
1206
+ resolverIdentityScope: createHash("sha256")
1207
+ .update(options.identityScope, "utf8")
1208
+ .digest("hex"),
1209
+ }
1210
+ : {}),
1211
+ };
1212
+ return adapter.solve(
982
1213
  challenge,
983
- solution,
984
- issuingIdentity,
985
- options.identityScope,
1214
+ identity,
1215
+ signal,
1216
+ telemetryTraceRecorder,
1217
+ transport,
1218
+ usage,
986
1219
  );
1220
+ };
1221
+ solution = traceRecorder
1222
+ ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
1223
+ attributes: {
1224
+ vendor: adapter.id,
1225
+ challenge_kind: challenge.kind,
1226
+ client_profile: options.clientProfile,
1227
+ resolver_identity_source: identityResolution.userAgentSource,
1228
+ },
1229
+ onError(error) {
1230
+ return error instanceof ResolverVendorUnavailableError
1231
+ ? unavailableSpanAttributes(error)
1232
+ : undefined;
1233
+ },
1234
+ })
1235
+ : await solveAttempt();
1236
+ } catch (error) {
1237
+ const unavailable = error instanceof ResolverVendorUnavailableError ? error : undefined;
1238
+ const attempt = unavailable ? unavailableAttempt(unavailable, true) : undefined;
1239
+ options.telemetry?.recordVendorAttempt({
1240
+ vendor: adapter.id,
1241
+ phase:
1242
+ resolverTelemetryPhase(unavailable?.phase) ??
1243
+ resolverTelemetryPhase(diagnostics.phase) ??
1244
+ "create_task",
1245
+ diagnostics: {
1246
+ ...attempt,
1247
+ attemptIndex: entryIndex + 1,
1248
+ phase: attempt?.phase ?? diagnostics.phase,
1249
+ },
1250
+ outcome: "error",
1251
+ ms: Date.now() - attemptStartedAt,
1252
+ errorClass: resolverAttemptErrorClass(error, signal),
1253
+ ...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
1254
+ ...(diagnostics.vendorErrorCode
1255
+ ? { vendorErrorCode: diagnostics.vendorErrorCode }
1256
+ : {}),
1257
+ ...(diagnostics.vendorErrorDescription
1258
+ ? { vendorErrorDescription: diagnostics.vendorErrorDescription }
1259
+ : {}),
1260
+ });
1261
+ signal.throwIfAborted();
1262
+ if (!unavailable || !attempt) throw error;
1263
+ attempts.push(attempt);
1264
+ const next = supportingEntries[entryIndex + 1];
1265
+ if (next) {
1266
+ options.telemetry?.recordFailover({
1267
+ from: adapter.id,
1268
+ to: next.id,
1269
+ reason: unavailable.reason,
1270
+ });
1271
+ }
1272
+ continue;
1273
+ }
1274
+ options.telemetry?.recordVendorAttempt({
1275
+ vendor: adapter.id,
1276
+ phase: resolverTelemetryPhase(diagnostics.phase) ?? "poll_result",
1277
+ diagnostics: { attemptIndex: entryIndex + 1, phase: diagnostics.phase },
1278
+ outcome: "ok",
1279
+ ms: Date.now() - attemptStartedAt,
1280
+ ...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
1281
+ });
1282
+ if (cache) {
1283
+ try {
1284
+ let cacheWrite: { written: boolean; reason?: "no_expires" | "not_cacheable" };
1285
+ if (!resolverChallengeIsCacheable(challenge)) {
1286
+ cacheWrite = { written: false, reason: "not_cacheable" };
1287
+ } else if (solution.form !== "cookies" || solutionExpiryMs(solution) === undefined) {
1288
+ cacheWrite = { written: false, reason: "no_expires" };
1289
+ } else {
1290
+ const issuingIdentity =
1291
+ adapter.getIssuingIdentity?.(solution, identity, challenge) ??
1292
+ resolverChallengeIssuingIdentity(challenge, {
1293
+ ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
1294
+ userAgent: solution.userAgent,
1295
+ });
1296
+ cacheWrite = issuingIdentity
1297
+ ? await cacheResolverSolution(
1298
+ cache,
1299
+ challenge,
1300
+ solution,
1301
+ issuingIdentity,
1302
+ options.identityScope,
1303
+ )
1304
+ : { written: false, reason: "not_cacheable" };
1305
+ }
1306
+ options.telemetry?.recordCacheWrite(cacheWrite);
1307
+ } catch (error) {
1308
+ options.telemetry?.recordCacheWrite({ written: false, reason: "error" });
1309
+ throw error;
987
1310
  }
988
1311
  }
989
1312
  rememberSolutionSource(solution, "vendor");
1313
+ // Adapter success includes SBSD payload acceptance with verified:false.
1314
+ // The caller reports protected-refetch verification separately.
1315
+ terminalOutcome = "solved";
990
1316
  return solution;
991
- } catch (error) {
992
- signal.throwIfAborted();
993
- if (!(error instanceof ResolverVendorUnavailableError)) throw error;
994
- // Every vendor-unavailable result, including missing_challenge_input, falls
995
- // through so another adapter can solve with a different input contract.
996
- attempts.push(unavailableAttempt(error));
997
1317
  }
998
- }
999
1318
 
1000
- throwExhausted(attempts);
1319
+ throwExhausted(challenge.kind, attempts);
1320
+ } catch (error) {
1321
+ terminalOutcome = signal.aborted
1322
+ ? "aborted"
1323
+ : error instanceof ProviderError && error.code === "RESOLVER_CHAIN_EXHAUSTED"
1324
+ ? "exhausted"
1325
+ : "error";
1326
+ throw error;
1327
+ } finally {
1328
+ options.telemetry?.recordOutcome({
1329
+ outcome: terminalOutcome,
1330
+ solveMs: Date.now() - solveStartedAt,
1331
+ challengeKind: challenge.kind,
1332
+ });
1333
+ }
1001
1334
  },
1002
1335
  };
1003
- resolverCaches.set(client, options.cache ?? null);
1336
+ resolverCaches.set(client, cache ?? null);
1337
+ const reusableOptions = { ...options, telemetry: undefined };
1338
+ registerResolverTelemetryBinding(client, createChainTelemetryBinding(reusableOptions));
1004
1339
  return client;
1005
1340
  }
1006
1341
 
@@ -1015,6 +1350,7 @@ export function createResolverClient(options: {
1015
1350
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
1016
1351
  readonly clientProfile?: string;
1017
1352
  readonly allowedHosts?: readonly string[];
1353
+ readonly telemetry?: ResolverTelemetrySink;
1018
1354
  }): ResolverChainClient {
1019
1355
  return createResolverChainClient({
1020
1356
  kinds: options.kinds,
@@ -1031,6 +1367,7 @@ export function createResolverClient(options: {
1031
1367
  createTransport: options.createTransport,
1032
1368
  clientProfile: options.clientProfile,
1033
1369
  allowedHosts: options.allowedHosts,
1370
+ telemetry: options.telemetry,
1034
1371
  });
1035
1372
  }
1036
1373
 
@@ -1058,7 +1395,9 @@ function resolveVendorAvailability(
1058
1395
  ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
1059
1396
  : vendor === "capsolver"
1060
1397
  ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
1061
- : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
1398
+ : vendor === "capmonster"
1399
+ ? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
1400
+ : APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
1062
1401
 
1063
1402
  const configuration = normalizedEnvValue(env, envKey);
1064
1403
  return configuration
@@ -1083,6 +1422,7 @@ export function bindResolverSignal(
1083
1422
  if (resolverCaches.has(resolver)) {
1084
1423
  resolverCaches.set(boundResolver, resolverCaches.get(resolver) ?? null);
1085
1424
  }
1425
+ inheritResolverTelemetryBinding(resolver, boundResolver);
1086
1426
  return boundResolver;
1087
1427
  }
1088
1428
 
@@ -1103,6 +1443,7 @@ function createResolverClientFromEnvInternal(
1103
1443
  kinds: config.kinds,
1104
1444
  entries: [],
1105
1445
  unavailableReason: "Provider resolver vendor chain is empty",
1446
+ telemetry: options.telemetry,
1106
1447
  });
1107
1448
  }
1108
1449
 
@@ -1135,6 +1476,7 @@ function createResolverClientFromEnvInternal(
1135
1476
  createTransport: options.createTransport,
1136
1477
  clientProfile: config.clientProfile,
1137
1478
  allowedHosts,
1479
+ telemetry: options.telemetry,
1138
1480
  });
1139
1481
  }
1140
1482