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