@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59

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 (107) hide show
  1. package/AUTHORING.md +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +22 -5
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-record.ts +6 -0
  7. package/dist/cli/templates/provider/README.md.tpl +19 -5
  8. package/dist/config/loader.d.ts +7 -0
  9. package/dist/config/loader.js +30 -6
  10. package/dist/declaration-validation.d.ts +2 -1
  11. package/dist/declaration-validation.js +39 -0
  12. package/dist/define.d.ts +5 -4
  13. package/dist/define.js +25 -1
  14. package/dist/engine.d.ts +16 -0
  15. package/dist/engine.js +58 -1
  16. package/dist/error-resolution.js +13 -0
  17. package/dist/index.d.ts +6 -4
  18. package/dist/index.js +4 -2
  19. package/dist/lint.js +3 -3
  20. package/dist/provider.d.ts +3 -3
  21. package/dist/provider.js +2 -2
  22. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  23. package/dist/runtime/bounded-expiring-map.js +64 -0
  24. package/dist/runtime/cache.d.ts +2 -0
  25. package/dist/runtime/cache.js +8 -1
  26. package/dist/runtime/egress-lease.d.ts +63 -0
  27. package/dist/runtime/egress-lease.js +233 -0
  28. package/dist/runtime/http.js +1 -0
  29. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  30. package/dist/runtime/proxy-retry-policy.js +11 -2
  31. package/dist/runtime/request-telemetry.d.ts +7 -9
  32. package/dist/runtime/resolver-config.d.ts +7 -0
  33. package/dist/runtime/resolver-config.js +8 -0
  34. package/dist/runtime/resolver-public.d.ts +3 -1
  35. package/dist/runtime/resolver-public.js +1 -1
  36. package/dist/runtime/resolver-shared.d.ts +9 -0
  37. package/dist/runtime/resolver-shared.js +57 -0
  38. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  39. package/dist/runtime/resolver-telemetry.js +204 -0
  40. package/dist/runtime/resolver-usage.d.ts +24 -0
  41. package/dist/runtime/resolver-usage.js +60 -0
  42. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  43. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  44. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  45. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  46. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  47. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  48. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  49. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  50. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  51. package/dist/runtime/resolver-vendors/types.js +20 -10
  52. package/dist/runtime/resolver.d.ts +5 -1
  53. package/dist/runtime/resolver.js +397 -109
  54. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  55. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  56. package/dist/runtime/stealth.js +547 -53
  57. package/dist/schema.d.ts +9 -0
  58. package/dist/schema.js +16 -0
  59. package/dist/server/index.d.ts +2 -0
  60. package/dist/server/index.js +1 -0
  61. package/dist/server/self-test.d.ts +4 -0
  62. package/dist/server/self-test.js +10 -1
  63. package/dist/server/serve-implementation.d.ts +7 -1
  64. package/dist/server/serve-implementation.js +181 -53
  65. package/dist/server/types.d.ts +6 -0
  66. package/dist/server/types.js +14 -0
  67. package/dist/stealth/profiles.d.ts +7 -1
  68. package/dist/stealth/profiles.js +9 -0
  69. package/dist/testing/run.js +6 -0
  70. package/dist/types.d.ts +115 -12
  71. package/package.json +1 -1
  72. package/src/cli/templates/provider/README.md.tpl +19 -5
  73. package/src/config/loader.ts +34 -6
  74. package/src/declaration-validation.ts +47 -1
  75. package/src/define.ts +46 -3
  76. package/src/engine.ts +76 -1
  77. package/src/error-resolution.ts +13 -0
  78. package/src/index.ts +32 -1
  79. package/src/lint.ts +3 -3
  80. package/src/provider.ts +9 -0
  81. package/src/runtime/bounded-expiring-map.ts +67 -0
  82. package/src/runtime/cache.ts +10 -1
  83. package/src/runtime/egress-lease.ts +336 -0
  84. package/src/runtime/http.ts +1 -0
  85. package/src/runtime/proxy-retry-policy.ts +12 -1
  86. package/src/runtime/request-telemetry.ts +5 -7
  87. package/src/runtime/resolver-config.ts +12 -0
  88. package/src/runtime/resolver-public.ts +21 -0
  89. package/src/runtime/resolver-shared.ts +75 -0
  90. package/src/runtime/resolver-telemetry.ts +362 -0
  91. package/src/runtime/resolver-usage.ts +96 -0
  92. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  93. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  94. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  95. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  96. package/src/runtime/resolver-vendors/types.ts +60 -12
  97. package/src/runtime/resolver.ts +465 -124
  98. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  99. package/src/runtime/stealth.ts +727 -95
  100. package/src/schema.ts +17 -0
  101. package/src/server/index.ts +21 -0
  102. package/src/server/self-test.ts +13 -0
  103. package/src/server/serve-implementation.ts +268 -48
  104. package/src/server/types.ts +14 -0
  105. package/src/stealth/profiles.ts +10 -0
  106. package/src/testing/run.ts +6 -0
  107. package/src/types.ts +130 -22
@@ -2,17 +2,19 @@ import { createHash } from "node:crypto";
2
2
  import { resolveProxyConfigAsync, } from "../config/loader.js";
3
3
  import { ProviderError } from "../errors.js";
4
4
  import { getStealthProfile } from "../stealth/profiles.js";
5
- import { resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
5
+ import { isProviderCacheBypassed } from "./cache.js";
6
+ import { RESOLVER_CHALLENGE_BINDINGS, resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
6
7
  import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
7
8
  import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
9
+ import { createHypersolutionsResolverVendorAdapter } from "./resolver-vendors/hypersolutions.js";
8
10
  import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
9
11
  import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
10
- import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
11
- import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
12
- import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
12
+ import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, attachResolverVendorPollObserver, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
13
+ import { createUnsupportedResolverClient, inheritResolverTelemetryBinding, registerResolverTelemetryBinding, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
14
+ import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, kindRequiresClientProfile, } from "./resolver-config.js";
13
15
  import { DEFAULT_STEALTH_PROFILE } from "./stealth.js";
14
16
  export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
15
- export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
17
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
16
18
  export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
17
19
  const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
18
20
  const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
@@ -81,6 +83,13 @@ const resolverAdapterRegistry = {
81
83
  timeoutMs,
82
84
  });
83
85
  },
86
+ hypersolutions(configuration, timeoutMs, allowedHosts) {
87
+ return createHypersolutionsResolverVendorAdapter({
88
+ allowedHosts,
89
+ apiKey: configuration,
90
+ timeoutMs,
91
+ });
92
+ },
84
93
  };
85
94
  export const RESOLVER_ADAPTER_REGISTRY = resolverAdapterRegistry;
86
95
  export function swapResolverAdapterFactoryForTests(vendor, factory) {
@@ -134,6 +143,12 @@ function readPositiveIntegerEnv(env, name) {
134
143
  return raw;
135
144
  }
136
145
  function assertDeclaredKind(requestedKind, declaredKinds) {
146
+ if (!Object.hasOwn(RESOLVER_CHALLENGE_BINDINGS, requestedKind)) {
147
+ throw new ProviderError(`Unknown resolver kind "${requestedKind}"`, {
148
+ code: "RESOLVER_KIND_NOT_DECLARED",
149
+ fix: "Use a challenge kind exported by @apifuse/provider-sdk.",
150
+ });
151
+ }
137
152
  if (declaredKinds.includes(requestedKind))
138
153
  return;
139
154
  const declared = declaredKinds.length > 0 ? declaredKinds.join(", ") : "none";
@@ -179,19 +194,20 @@ function throwUnsupportedKind(kind, usingDefaultVendors) {
179
194
  : `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
180
195
  });
181
196
  }
182
- function throwExhausted(attempts) {
197
+ function throwExhausted(challengeKind, attempts, executedAttemptCount = attempts.length) {
183
198
  const hasMissingChallengeInput = attempts.some((attempt) => attempt.reason === "missing_challenge_input");
184
- const summary = attempts
185
- .map(({ vendor, reason, missingFields }) => missingFields === undefined || missingFields.length === 0
186
- ? `${vendor}: ${reason}`
187
- : `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`)
188
- .join(", ");
189
- throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
199
+ throw new ProviderError("The challenge could not be resolved.", {
190
200
  code: "RESOLVER_CHAIN_EXHAUSTED",
191
201
  fix: hasMissingChallengeInput
192
- ? "Capture the named challenge fields or configure another supporting resolver vendor."
193
- : "Configure another supporting resolver vendor or restore an unavailable vendor.",
194
- details: attempts,
202
+ ? "Capture all required challenge fields and retry the request."
203
+ : "Retry the request or contact support if the challenge continues.",
204
+ retryable: false,
205
+ details: {
206
+ challengeKind,
207
+ attempts: executedAttemptCount,
208
+ outcome: "exhausted",
209
+ retryable: false,
210
+ },
195
211
  });
196
212
  }
197
213
  function assertClientProfileTransportContract(clientProfile, transport) {
@@ -245,6 +261,16 @@ function sanitizeCauseMessage(message) {
245
261
  }
246
262
  function restrictResolverTransport(transport, allowedHosts) {
247
263
  return {
264
+ sessionHeaders: transport.sessionHeaders,
265
+ ...(transport.getCookie
266
+ ? {
267
+ getCookie(name, url) {
268
+ // Jar reads are gated like fetches: an adapter may only read state for declared hosts.
269
+ assertResolverHostAllowed(url, allowedHosts);
270
+ return transport.getCookie?.(name, url);
271
+ },
272
+ }
273
+ : {}),
248
274
  async fetch(url, init) {
249
275
  // Empty declarations remain deny-by-default, matching the adapter-factory/browser path.
250
276
  assertResolverHostAllowed(url, allowedHosts);
@@ -286,10 +312,17 @@ function safeUpstreamHost(host) {
286
312
  function safePhase(phase) {
287
313
  return phase !== undefined && /^[a-z\d_.:-]{1,64}$/i.test(phase) ? phase : undefined;
288
314
  }
289
- function unavailableAttempt(error) {
290
- const cause = safeCause(error);
315
+ function unavailableAttempt(error, forTelemetry = false) {
316
+ // Preserve the full input for the collector's redact-then-bound path. Span
317
+ // attributes retain their existing sanitization and phase validation.
318
+ const cause = forTelemetry && error.cause !== undefined
319
+ ? {
320
+ name: error.cause instanceof Error ? error.cause.name : "Error",
321
+ message: error.cause instanceof Error ? error.cause.message : String(error.cause),
322
+ }
323
+ : safeCause(error);
291
324
  const upstreamHost = safeUpstreamHost(error.upstreamHost);
292
- const phase = safePhase(error.phase);
325
+ const phase = forTelemetry ? error.phase : safePhase(error.phase);
293
326
  const round = Number.isSafeInteger(error.round) && error.round > 0 ? error.round : undefined;
294
327
  return {
295
328
  vendor: error.vendor,
@@ -313,6 +346,103 @@ function unavailableSpanAttributes(error) {
313
346
  transport_round: attempt.round,
314
347
  };
315
348
  }
349
+ function resolverTelemetryPhase(value) {
350
+ return value === "create_task" ||
351
+ value === "poll_result" ||
352
+ value === "cleanup" ||
353
+ value === "measure_ip" ||
354
+ value === "fetch_script" ||
355
+ value === "generate_payload" ||
356
+ value === "post_payload"
357
+ ? value
358
+ : undefined;
359
+ }
360
+ function diagnosticString(value) {
361
+ return typeof value === "string" && value.length > 0 ? value : undefined;
362
+ }
363
+ function diagnosticCount(value) {
364
+ return typeof value === "number" && Number.isFinite(value)
365
+ ? Math.max(0, Math.floor(value))
366
+ : undefined;
367
+ }
368
+ function createResolverTelemetryTraceRecorder(input) {
369
+ if (!input.base && !input.telemetry)
370
+ return undefined;
371
+ const recorder = {
372
+ async runSpan(name, fn, spanOptions = {}) {
373
+ const adapterPhase = name.startsWith("resolver.vendor.")
374
+ ? diagnosticString(name.replace("resolver.vendor.", ""))
375
+ : undefined;
376
+ const phase = resolverTelemetryPhase(adapterPhase);
377
+ const startedAt = Date.now();
378
+ let attributes;
379
+ let spanOutcome = "ok";
380
+ const wrappedOptions = {
381
+ ...spanOptions,
382
+ onSuccess(value) {
383
+ attributes = spanOptions.onSuccess?.(value);
384
+ return attributes;
385
+ },
386
+ onError(error) {
387
+ attributes = spanOptions.onError?.(error);
388
+ return attributes;
389
+ },
390
+ };
391
+ try {
392
+ if (input.base)
393
+ return await input.base.runSpan(name, fn, wrappedOptions);
394
+ const value = await fn();
395
+ attributes = spanOptions.onSuccess?.(value);
396
+ return value;
397
+ }
398
+ catch (error) {
399
+ spanOutcome = "error";
400
+ if (!input.base)
401
+ attributes = spanOptions.onError?.(error);
402
+ throw error;
403
+ }
404
+ finally {
405
+ if (adapterPhase && adapterPhase !== "attempt") {
406
+ const reportedPhase = diagnosticString(attributes?.transport_phase) ?? adapterPhase;
407
+ const pollCount = diagnosticCount(attributes?.poll_count);
408
+ const vendorErrorCode = diagnosticString(attributes?.vendor_error_code);
409
+ const vendorErrorDescription = diagnosticString(attributes?.vendor_error_description ?? attributes?.error_message);
410
+ if (phase === "cleanup") {
411
+ input.telemetry?.recordVendorAttempt({
412
+ vendor: input.vendor,
413
+ phase: "cleanup",
414
+ diagnostics: { phase: reportedPhase, attemptIndex: input.attemptIndex },
415
+ outcome: spanOutcome,
416
+ ms: Date.now() - startedAt,
417
+ ...(pollCount === undefined ? {} : { pollCount }),
418
+ ...(vendorErrorCode ? { vendorErrorCode } : {}),
419
+ ...(vendorErrorDescription ? { vendorErrorDescription } : {}),
420
+ ...(spanOutcome === "error" ? { errorClass: "unexpected" } : {}),
421
+ });
422
+ }
423
+ else {
424
+ input.diagnostics.phase = reportedPhase;
425
+ if (pollCount !== undefined)
426
+ input.diagnostics.pollCount = pollCount;
427
+ if (vendorErrorCode)
428
+ input.diagnostics.vendorErrorCode = vendorErrorCode;
429
+ if (vendorErrorDescription) {
430
+ input.diagnostics.vendorErrorDescription = vendorErrorDescription;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ },
436
+ };
437
+ return attachResolverVendorPollObserver(recorder, () => {
438
+ input.diagnostics.pollCount = (input.diagnostics.pollCount ?? 0) + 1;
439
+ });
440
+ }
441
+ function resolverAttemptErrorClass(error, signal) {
442
+ if (signal.aborted)
443
+ return "aborted";
444
+ return error instanceof ResolverVendorUnavailableError ? error.reason : "unexpected";
445
+ }
316
446
  function challengeOrigin(challenge) {
317
447
  return new URL(challenge.pageUrl).origin;
318
448
  }
@@ -448,27 +578,27 @@ async function writeResolverCacheIndex(cache, challenge, entries, now) {
448
578
  }
449
579
  async function cacheResolverSolution(cache, challenge, solution, identity, identityScope) {
450
580
  if (!resolverChallengeIsCacheable(challenge))
451
- return;
581
+ return { written: false, reason: "not_cacheable" };
452
582
  const expiresAtMs = solutionExpiryMs(solution);
453
583
  const now = Date.now();
454
584
  if (expiresAtMs === undefined)
455
- return;
585
+ return { written: false, reason: "no_expires" };
456
586
  const ttlMs = Math.floor(expiresAtMs - now);
457
587
  if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS)
458
- return;
588
+ return { written: false, reason: "no_expires" };
459
589
  const scopedDigest = identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
460
590
  ? resolverIdentityScopeDigest(identityScope)
461
591
  : undefined;
462
592
  if (!resolverChallengeAllowsDirectCache(challenge) &&
463
593
  scopedDigest === undefined &&
464
594
  identity.proxyUrl === undefined) {
465
- return;
595
+ return { written: false, reason: "not_cacheable" };
466
596
  }
467
597
  const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
468
598
  await cache.set(resolverSolutionCacheKey(cache, challenge, issuerDigest), { expiresAtMs, issuerDigest, solution }, { ttlMs });
469
599
  rememberSolutionIssuer(solution, issuerDigest);
470
600
  if (scopedDigest !== undefined || identity.proxyUrl !== undefined)
471
- return;
601
+ return { written: true };
472
602
  const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
473
603
  const current = await cache.get(indexKey);
474
604
  const currentEntries = isResolverCacheIndex(current?.value) ? current.value.entries : [];
@@ -476,6 +606,7 @@ async function cacheResolverSolution(cache, challenge, solution, identity, ident
476
606
  ...currentEntries.filter((entry) => entry.issuerDigest !== issuerDigest),
477
607
  { direct: true, expiresAtMs, issuerDigest },
478
608
  ], now);
609
+ return { written: true };
479
610
  }
480
611
  async function invalidateResolverSolutionWithOutcome(resolver, challenge, solution) {
481
612
  const metadata = resolver[RESOLVER_INSTRUMENTATION_METADATA];
@@ -566,107 +697,258 @@ async function resolveResolverIdentity(proxyIntent) {
566
697
  return { unavailableReason: "missing_client_profile", userAgentSource };
567
698
  }
568
699
  }
700
+ function createChainTelemetryBinding(options) {
701
+ return (telemetry) => createResolverChainClient({ ...options, telemetry });
702
+ }
569
703
  function createResolverChainClient(options) {
570
704
  assertClientProfileTransportContract(options.clientProfile, options.transport);
705
+ const cache = options.cache && !isProviderCacheBypassed(options.cache) ? options.cache : undefined;
571
706
  const client = {
572
707
  async solve(challenge, signal = new AbortController().signal, traceRecorder) {
573
- assertDeclaredKind(challenge.kind, options.kinds);
574
- if (options.unavailableReason) {
575
- throw new ProviderError(options.unavailableReason, {
576
- code: "RESOLVER_UNAVAILABLE",
577
- fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
708
+ const solveStartedAt = Date.now();
709
+ let terminalOutcome = "error";
710
+ try {
711
+ assertDeclaredKind(challenge.kind, options.kinds);
712
+ if (options.unavailableReason) {
713
+ throw new ProviderError(options.unavailableReason, {
714
+ code: "RESOLVER_UNAVAILABLE",
715
+ fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
716
+ });
717
+ }
718
+ const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
719
+ if (supportingEntries.length === 0) {
720
+ throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
721
+ }
722
+ if (kindRequiresClientProfile(challenge.kind) && !options.clientProfile?.trim()) {
723
+ options.telemetry?.recordIdentity({ source: "none", failure: "missing_client_profile" });
724
+ throwExhausted(challenge.kind, supportingEntries.map((entry) => ({
725
+ vendor: entry.id,
726
+ reason: "missing_client_profile",
727
+ })), 0);
728
+ }
729
+ signal.throwIfAborted();
730
+ const identityResolution = options.proxyIntent
731
+ ? await resolveResolverIdentity(options.proxyIntent)
732
+ : { identity: options.identity };
733
+ const identity = identityResolution.identity;
734
+ options.telemetry?.recordIdentity({
735
+ // For SBSD, "declared" identifies the configured client profile and its bound
736
+ // session identity; the engine independently supplies solver credentials.
737
+ source: kindRequiresClientProfile(challenge.kind) && options.clientProfile
738
+ ? "declared"
739
+ : options.proxyIntent
740
+ ? (identityResolution.userAgentSource ?? "defaulted")
741
+ : identity
742
+ ? "declared"
743
+ : "none",
744
+ ...(identityResolution.unavailableReason
745
+ ? { failure: identityResolution.unavailableReason }
746
+ : {}),
578
747
  });
579
- }
580
- const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
581
- if (supportingEntries.length === 0) {
582
- throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
583
- }
584
- signal.throwIfAborted();
585
- const identityResolution = options.proxyIntent
586
- ? await resolveResolverIdentity(options.proxyIntent)
587
- : { identity: options.identity };
588
- const identity = identityResolution.identity;
589
- signal.throwIfAborted();
590
- // Resolve a required proxy lease before consulting the cache. Solutions minted
591
- // under a previous release are shared and long-lived, but a portable cached token
592
- // must not bypass the upstream admission policy when no lease can be resolved.
593
- const requiredProxyIdentityMissing = options.proxyIntent?.mode === "required" && identity === undefined;
594
- if (requiredProxyIdentityMissing) {
595
- throwExhausted(supportingEntries.map((entry) => ({
596
- vendor: entry.id,
597
- reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
598
- })));
599
- }
600
- if (options.cache && resolverChallengeIsCacheable(challenge)) {
601
- const cached = await findCachedSolution(options.cache, challenge, identity, options.identityScope);
602
- if (cached)
603
- return cached;
604
- }
605
- const attempts = [];
606
- for (const entry of supportingEntries) {
607
- const adapter = entry.createAdapter();
608
- try {
609
- const solveAttempt = () => {
610
- const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
611
- const unrestrictedTransport = options.transport ??
612
- (requiresTransport
613
- ? options.createTransport?.({
614
- clientProfile: options.clientProfile,
615
- identityScope: options.identityScope,
616
- })
617
- : undefined);
618
- if (requiresTransport && unrestrictedTransport === undefined) {
619
- throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
748
+ signal.throwIfAborted();
749
+ const requiredProxyIdentityMissing = options.proxyIntent?.mode === "required" && identity === undefined;
750
+ if (requiredProxyIdentityMissing) {
751
+ const reason = identityResolution.unavailableReason ?? "missing_proxy_identity";
752
+ for (const [entryIndex, entry] of supportingEntries.entries()) {
753
+ options.telemetry?.recordVendorAttempt({
754
+ vendor: entry.id,
755
+ phase: "create_task",
756
+ diagnostics: { attemptIndex: entryIndex + 1 },
757
+ outcome: "error",
758
+ ms: 0,
759
+ errorClass: reason,
760
+ });
761
+ const next = supportingEntries[entryIndex + 1];
762
+ if (next) {
763
+ options.telemetry?.recordFailover({ from: entry.id, to: next.id, reason });
620
764
  }
621
- const transport = unrestrictedTransport
622
- ? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
623
- : undefined;
624
- return adapter.solve(challenge, identity, signal, traceRecorder, transport);
625
- };
626
- const solution = traceRecorder
627
- ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
628
- attributes: {
629
- vendor: adapter.id,
630
- challenge_kind: challenge.kind,
631
- client_profile: options.clientProfile,
632
- resolver_identity_source: identityResolution.userAgentSource,
633
- },
634
- onError(error) {
635
- return error instanceof ResolverVendorUnavailableError
636
- ? unavailableSpanAttributes(error)
637
- : undefined;
765
+ }
766
+ throwExhausted(challenge.kind, supportingEntries.map((entry) => ({
767
+ vendor: entry.id,
768
+ reason,
769
+ })));
770
+ }
771
+ if (!cache) {
772
+ options.telemetry?.recordCacheRead({ status: "disabled", challengeKind: challenge.kind });
773
+ }
774
+ else if (!resolverChallengeIsCacheable(challenge)) {
775
+ options.telemetry?.recordCacheRead({
776
+ status: "not_cacheable",
777
+ challengeKind: challenge.kind,
778
+ });
779
+ }
780
+ else {
781
+ const cached = await findCachedSolution(cache, challenge, identity, options.identityScope);
782
+ options.telemetry?.recordCacheRead({
783
+ status: cached ? "hit" : "miss",
784
+ challengeKind: challenge.kind,
785
+ });
786
+ if (cached) {
787
+ terminalOutcome = "cached";
788
+ return cached;
789
+ }
790
+ }
791
+ const attempts = [];
792
+ for (const [entryIndex, entry] of supportingEntries.entries()) {
793
+ const adapter = entry.createAdapter();
794
+ const attemptStartedAt = Date.now();
795
+ const diagnostics = {};
796
+ const telemetryTraceRecorder = createResolverTelemetryTraceRecorder({
797
+ base: traceRecorder,
798
+ vendor: adapter.id,
799
+ attemptIndex: entryIndex + 1,
800
+ telemetry: options.telemetry,
801
+ diagnostics,
802
+ });
803
+ let solution;
804
+ try {
805
+ const solveAttempt = () => {
806
+ const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
807
+ const unrestrictedTransport = options.transport ??
808
+ (requiresTransport
809
+ ? options.createTransport?.({
810
+ clientProfile: options.clientProfile,
811
+ identityScope: options.identityScope,
812
+ })
813
+ : undefined);
814
+ if (requiresTransport && unrestrictedTransport === undefined) {
815
+ throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
816
+ }
817
+ const transport = unrestrictedTransport
818
+ ? restrictResolverTransport(unrestrictedTransport, [
819
+ ...(options.allowedHosts ?? []),
820
+ ...(adapter.transportAllowedHosts ?? []),
821
+ ])
822
+ : undefined;
823
+ const usage = {
824
+ // Position in the declared chain, not in the kind-filtered list.
825
+ vendorIndex: options.entries.indexOf(entry) + 1,
826
+ ...(options.identityScope
827
+ ? {
828
+ resolverIdentityScope: createHash("sha256")
829
+ .update(options.identityScope, "utf8")
830
+ .digest("hex"),
831
+ }
832
+ : {}),
833
+ };
834
+ return adapter.solve(challenge, identity, signal, telemetryTraceRecorder, transport, usage);
835
+ };
836
+ solution = traceRecorder
837
+ ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
838
+ attributes: {
839
+ vendor: adapter.id,
840
+ challenge_kind: challenge.kind,
841
+ client_profile: options.clientProfile,
842
+ resolver_identity_source: identityResolution.userAgentSource,
843
+ },
844
+ onError(error) {
845
+ return error instanceof ResolverVendorUnavailableError
846
+ ? unavailableSpanAttributes(error)
847
+ : undefined;
848
+ },
849
+ })
850
+ : await solveAttempt();
851
+ }
852
+ catch (error) {
853
+ const unavailable = error instanceof ResolverVendorUnavailableError ? error : undefined;
854
+ const attempt = unavailable ? unavailableAttempt(unavailable, true) : undefined;
855
+ options.telemetry?.recordVendorAttempt({
856
+ vendor: adapter.id,
857
+ phase: resolverTelemetryPhase(unavailable?.phase) ??
858
+ resolverTelemetryPhase(diagnostics.phase) ??
859
+ "create_task",
860
+ diagnostics: {
861
+ ...attempt,
862
+ attemptIndex: entryIndex + 1,
863
+ phase: attempt?.phase ?? diagnostics.phase,
638
864
  },
639
- })
640
- : await solveAttempt();
641
- if (options.cache &&
642
- resolverChallengeIsCacheable(challenge) &&
643
- solution.form === "cookies" &&
644
- solutionExpiryMs(solution) !== undefined) {
645
- const issuingIdentity = adapter.getIssuingIdentity?.(solution, identity, challenge) ??
646
- resolverChallengeIssuingIdentity(challenge, {
647
- ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
648
- userAgent: solution.userAgent,
865
+ outcome: "error",
866
+ ms: Date.now() - attemptStartedAt,
867
+ errorClass: resolverAttemptErrorClass(error, signal),
868
+ ...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
869
+ ...(diagnostics.vendorErrorCode
870
+ ? { vendorErrorCode: diagnostics.vendorErrorCode }
871
+ : {}),
872
+ ...(diagnostics.vendorErrorDescription
873
+ ? { vendorErrorDescription: diagnostics.vendorErrorDescription }
874
+ : {}),
875
+ });
876
+ signal.throwIfAborted();
877
+ if (!unavailable || !attempt)
878
+ throw error;
879
+ attempts.push(attempt);
880
+ const next = supportingEntries[entryIndex + 1];
881
+ if (next) {
882
+ options.telemetry?.recordFailover({
883
+ from: adapter.id,
884
+ to: next.id,
885
+ reason: unavailable.reason,
649
886
  });
650
- if (issuingIdentity) {
651
- await cacheResolverSolution(options.cache, challenge, solution, issuingIdentity, options.identityScope);
887
+ }
888
+ continue;
889
+ }
890
+ options.telemetry?.recordVendorAttempt({
891
+ vendor: adapter.id,
892
+ phase: resolverTelemetryPhase(diagnostics.phase) ?? "poll_result",
893
+ diagnostics: { attemptIndex: entryIndex + 1, phase: diagnostics.phase },
894
+ outcome: "ok",
895
+ ms: Date.now() - attemptStartedAt,
896
+ ...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
897
+ });
898
+ if (cache) {
899
+ try {
900
+ let cacheWrite;
901
+ if (!resolverChallengeIsCacheable(challenge)) {
902
+ cacheWrite = { written: false, reason: "not_cacheable" };
903
+ }
904
+ else if (solution.form !== "cookies" || solutionExpiryMs(solution) === undefined) {
905
+ cacheWrite = { written: false, reason: "no_expires" };
906
+ }
907
+ else {
908
+ const issuingIdentity = adapter.getIssuingIdentity?.(solution, identity, challenge) ??
909
+ resolverChallengeIssuingIdentity(challenge, {
910
+ ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
911
+ userAgent: solution.userAgent,
912
+ });
913
+ cacheWrite = issuingIdentity
914
+ ? await cacheResolverSolution(cache, challenge, solution, issuingIdentity, options.identityScope)
915
+ : { written: false, reason: "not_cacheable" };
916
+ }
917
+ options.telemetry?.recordCacheWrite(cacheWrite);
918
+ }
919
+ catch (error) {
920
+ options.telemetry?.recordCacheWrite({ written: false, reason: "error" });
921
+ throw error;
652
922
  }
653
923
  }
654
924
  rememberSolutionSource(solution, "vendor");
925
+ // Adapter success includes SBSD payload acceptance with verified:false.
926
+ // The caller reports protected-refetch verification separately.
927
+ terminalOutcome = "solved";
655
928
  return solution;
656
929
  }
657
- catch (error) {
658
- signal.throwIfAborted();
659
- if (!(error instanceof ResolverVendorUnavailableError))
660
- throw error;
661
- // Every vendor-unavailable result, including missing_challenge_input, falls
662
- // through so another adapter can solve with a different input contract.
663
- attempts.push(unavailableAttempt(error));
664
- }
930
+ throwExhausted(challenge.kind, attempts);
931
+ }
932
+ catch (error) {
933
+ terminalOutcome = signal.aborted
934
+ ? "aborted"
935
+ : error instanceof ProviderError && error.code === "RESOLVER_CHAIN_EXHAUSTED"
936
+ ? "exhausted"
937
+ : "error";
938
+ throw error;
939
+ }
940
+ finally {
941
+ options.telemetry?.recordOutcome({
942
+ outcome: terminalOutcome,
943
+ solveMs: Date.now() - solveStartedAt,
944
+ challengeKind: challenge.kind,
945
+ });
665
946
  }
666
- throwExhausted(attempts);
667
947
  },
668
948
  };
669
- resolverCaches.set(client, options.cache ?? null);
949
+ resolverCaches.set(client, cache ?? null);
950
+ const reusableOptions = { ...options, telemetry: undefined };
951
+ registerResolverTelemetryBinding(client, createChainTelemetryBinding(reusableOptions));
670
952
  return client;
671
953
  }
672
954
  export function createResolverClient(options) {
@@ -685,6 +967,7 @@ export function createResolverClient(options) {
685
967
  createTransport: options.createTransport,
686
968
  clientProfile: options.clientProfile,
687
969
  allowedHosts: options.allowedHosts,
970
+ telemetry: options.telemetry,
688
971
  });
689
972
  }
690
973
  function resolveVendorAvailability(vendor, env) {
@@ -706,7 +989,9 @@ function resolveVendorAvailability(vendor, env) {
706
989
  ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
707
990
  : vendor === "capsolver"
708
991
  ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
709
- : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
992
+ : vendor === "capmonster"
993
+ ? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
994
+ : APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
710
995
  const configuration = normalizedEnvValue(env, envKey);
711
996
  return configuration
712
997
  ? { vendor, available: true, configuration }
@@ -723,6 +1008,7 @@ export function bindResolverSignal(resolver, defaultSignal) {
723
1008
  if (resolverCaches.has(resolver)) {
724
1009
  resolverCaches.set(boundResolver, resolverCaches.get(resolver) ?? null);
725
1010
  }
1011
+ inheritResolverTelemetryBinding(resolver, boundResolver);
726
1012
  return boundResolver;
727
1013
  }
728
1014
  function createResolverClientFromEnvInternal(config, env, options, adapterFactories) {
@@ -736,6 +1022,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
736
1022
  kinds: config.kinds,
737
1023
  entries: [],
738
1024
  unavailableReason: "Provider resolver vendor chain is empty",
1025
+ telemetry: options.telemetry,
739
1026
  });
740
1027
  }
741
1028
  const timeoutValue = readPositiveIntegerEnv(env, APIFUSE__RESOLVER__TIMEOUT_MS);
@@ -760,6 +1047,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
760
1047
  createTransport: options.createTransport,
761
1048
  clientProfile: config.clientProfile,
762
1049
  allowedHosts,
1050
+ telemetry: options.telemetry,
763
1051
  });
764
1052
  }
765
1053
  export function createResolverClientFromEnv(config, env = process.env, options = {}) {