@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 "../runtime/diagnostic-env.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { existsSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
@@ -6,6 +7,7 @@ import path from "node:path";
6
7
  import type { Redis } from "ioredis";
7
8
 
8
9
  import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
10
+ import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
9
11
  import {
10
12
  NODEMAVEN_DEFAULT_PROTOCOL,
11
13
  NODEMAVEN_FILTER_ENV,
@@ -223,7 +225,13 @@ type ProxyRedisClient = Pick<
223
225
  "connect" | "del" | "eval" | "get" | "on" | "pttl" | "set" | "status"
224
226
  >;
225
227
 
226
- const proxyCache = new Map<string, CachedProxyPool>();
228
+ // Keyed per (policy, affinity). Connection affinities are minted by callers, so
229
+ // without a cap a long-lived process accumulates one pool per affinity it ever
230
+ // saw. Evicting a still-fresh pool forces a fresh allocation (a different
231
+ // egress endpoint) for that affinity, so the cap is generous and every
232
+ // capacity eviction is reported as `diagnostics.poolCacheEvictions`.
233
+ const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
234
+ let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
227
235
  const require = createRequire(import.meta.url);
228
236
  const proxyInflight = new Map<string, Promise<SmartproxyAllocationResult>>();
229
237
  const invalidatedProxyKeys = new Map<string, number>();
@@ -249,8 +257,8 @@ const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
249
257
 
250
258
  function redisUrlFromEnv(): string | undefined {
251
259
  return (
252
- process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
253
- process.env[REDIS_URL_ENV]?.trim() ||
260
+ readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
261
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
254
262
  undefined
255
263
  );
256
264
  }
@@ -261,9 +269,9 @@ export function providerCacheRedisUrlFromEnv(): string | undefined {
261
269
 
262
270
  export function providerStateRedisUrlFromEnv(): string | undefined {
263
271
  return (
264
- process.env[PROVIDER_STATE_REDIS_URL_ENV]?.trim() ||
265
- process.env[PROVIDER_CACHE_REDIS_URL_ENV]?.trim() ||
266
- process.env[REDIS_URL_ENV]?.trim() ||
272
+ readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
273
+ readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
274
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
267
275
  undefined
268
276
  );
269
277
  }
@@ -277,6 +285,22 @@ export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number |
277
285
  smartproxyAllocatorDeadlineMsForTests = deadlineMs;
278
286
  }
279
287
 
288
+ /** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
289
+ export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void {
290
+ proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
291
+ }
292
+
293
+ /** @internal Test-only view of the in-memory Smartproxy cache sizes. */
294
+ export function __getSmartproxyCacheSizesForTests(): { pools: number; tombstones: number } {
295
+ return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
296
+ }
297
+
298
+ function createSmartproxyPoolCache(
299
+ maxEntries: number,
300
+ ): BoundedExpiringMap<string, CachedProxyPool> {
301
+ return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
302
+ }
303
+
280
304
  function getProxyRedis(): ProxyRedisClient | undefined {
281
305
  if (proxyRedisForTests) return proxyRedisForTests;
282
306
  const redisUrl = redisUrlFromEnv();
@@ -670,12 +694,12 @@ export async function resolveWithVendor(
670
694
  const startedAt = Date.now();
671
695
  const username = (
672
696
  context.credentials === undefined
673
- ? process.env[NODEMAVEN_USERNAME_ENV]
697
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
674
698
  : context.credentials[NODEMAVEN_USERNAME_ENV]
675
699
  )?.trim();
676
700
  const password = (
677
701
  context.credentials === undefined
678
- ? process.env[NODEMAVEN_PASSWORD_ENV]
702
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
679
703
  : context.credentials[NODEMAVEN_PASSWORD_ENV]
680
704
  )?.trim();
681
705
  const filter =
@@ -726,7 +750,7 @@ export async function resolveWithVendor(
726
750
  // smartproxy allocation-style vendor.
727
751
  const appKey = (
728
752
  context.credentials === undefined
729
- ? process.env[SMARTPROXY_APP_KEY_ENV]
753
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
730
754
  : context.credentials[SMARTPROXY_APP_KEY_ENV]
731
755
  )?.trim();
732
756
  if (!appKey) {
@@ -760,6 +784,7 @@ export async function resolveWithVendor(
760
784
  ...allocated.pool.diagnostics,
761
785
  poolSize: allocated.pool.urls.length,
762
786
  poolIndex,
787
+ poolCacheEvictions: proxyCache.capacityEvictions,
763
788
  },
764
789
  };
765
790
  }
@@ -814,18 +839,18 @@ function vendorHasCredentials(
814
839
  if (vendor === "nodemaven") {
815
840
  return Boolean(
816
841
  (credentials === undefined
817
- ? process.env[NODEMAVEN_USERNAME_ENV]
842
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
818
843
  : credentials[NODEMAVEN_USERNAME_ENV]
819
844
  )?.trim() &&
820
845
  (credentials === undefined
821
- ? process.env[NODEMAVEN_PASSWORD_ENV]
846
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
822
847
  : credentials[NODEMAVEN_PASSWORD_ENV]
823
848
  )?.trim(),
824
849
  );
825
850
  }
826
851
  return Boolean(
827
852
  (credentials === undefined
828
- ? process.env[SMARTPROXY_APP_KEY_ENV]
853
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
829
854
  : credentials[SMARTPROXY_APP_KEY_ENV]
830
855
  )?.trim(),
831
856
  );
@@ -1082,8 +1107,8 @@ async function allocateSmartproxy(
1082
1107
  const now = startedAt;
1083
1108
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
1084
1109
  const skipCached = invalidatedUntil > now;
1085
- const cached = proxyCache.get(cacheKey);
1086
- if (!skipCached && cached && isFresh(cached, now)) {
1110
+ const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
1111
+ if (cached) {
1087
1112
  if (shouldSoftRefresh(cached, now)) {
1088
1113
  void refreshSmartproxyPool(
1089
1114
  cacheKey,
@@ -1158,7 +1183,7 @@ async function readSmartproxyRedisPool(
1158
1183
  }
1159
1184
  const now = Date.now();
1160
1185
  if (!isFresh(pool, now)) return null;
1161
- proxyCache.set(cacheKey, pool);
1186
+ proxyCache.set(cacheKey, pool, now);
1162
1187
  return {
1163
1188
  pool,
1164
1189
  telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
@@ -1482,7 +1507,7 @@ async function allocateAndStoreSmartproxyPool(
1482
1507
  rawConnect: true,
1483
1508
  },
1484
1509
  };
1485
- proxyCache.set(cacheKey, result);
1510
+ proxyCache.set(cacheKey, result, allocatedAt);
1486
1511
  if (options.redis && options.poolKey) {
1487
1512
  const redis = options.redis;
1488
1513
  const poolKey = options.poolKey;
@@ -1733,7 +1758,7 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
1733
1758
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1734
1759
  const appKey = (
1735
1760
  options.engineCredentials === undefined
1736
- ? process.env[SMARTPROXY_APP_KEY_ENV]
1761
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
1737
1762
  : options.engineCredentials[SMARTPROXY_APP_KEY_ENV]
1738
1763
  )?.trim();
1739
1764
  if (!appKey) return undefined;
@@ -1744,7 +1769,11 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
1744
1769
  lifetimeMinutes,
1745
1770
  options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
1746
1771
  );
1747
- invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1772
+ const now = Date.now();
1773
+ for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
1774
+ if (invalidatedUntil <= now) invalidatedProxyKeys.delete(key);
1775
+ }
1776
+ invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1748
1777
  proxyCache.delete(cacheKey);
1749
1778
  proxyInflight.delete(cacheKey);
1750
1779
  return cacheKey;
@@ -1,6 +1,8 @@
1
1
  import { describeSchema } from "./contract-serialization.js";
2
2
  import { ProviderError } from "./errors.js";
3
3
  import { HealthScenarioSchema } from "./health-scenario.js";
4
+ import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
5
+ import { resolverClientProfileFamily, resolveStealthProfileSelection } from "./stealth/profiles.js";
4
6
  import type {
5
7
  HealthJourneyDefinition,
6
8
  ProviderDefinition,
@@ -22,6 +24,7 @@ export const DECLARATION_RULE_IDS = {
22
24
  proxySmartproxyGeo: "proxy-smartproxy-country-only",
23
25
  operationUpstreamProxy: "operation-upstream-proxy-unsupported",
24
26
  pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
27
+ resolverClientProfileFamily: "resolver-client-profile-family",
25
28
  } as const;
26
29
 
27
30
  export type DeclarationRuleId =
@@ -57,11 +60,15 @@ export function validateFailClosedDeclaration(provider: ProviderDefinition): voi
57
60
  validatePinnedWireFieldPaths(provider, violations);
58
61
  validateSchemaDeclaration(provider, violations);
59
62
  validateProxyDeclaration(provider, violations);
63
+ validateResolverClientProfileDeclaration(provider, violations);
60
64
  validateOperationDeclaration(provider, violations);
61
65
  if (violations.length > 0) throw declarationInvalidError(violations);
62
66
  }
63
67
 
64
- type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
68
+ type ProviderDeclarationRulesInput = Pick<
69
+ ProviderDefinition,
70
+ "healthJourneys" | "meta" | "proxy" | "resolver" | "stealth"
71
+ >;
65
72
  type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
66
73
 
67
74
  /** Enforces fail-closed rules that only depend on the provider declaration. */
@@ -89,6 +96,7 @@ function collectProviderDeclarationViolations(
89
96
  validateHealthDeclaration(provider, violations);
90
97
  validatePinnedWireFieldPaths(provider, violations);
91
98
  validateProxyDeclaration(provider, violations);
99
+ validateResolverClientProfileDeclaration(provider, violations);
92
100
  }
93
101
 
94
102
  function validatePinnedWireFieldPaths(
@@ -307,6 +315,44 @@ function validateProxyDeclaration(
307
315
  }
308
316
  }
309
317
 
318
+ /**
319
+ * An akamai_sbsd solve runs on the provider's own stealth session, so the declared
320
+ * resolver.clientProfile must name the browser family that session emulates. The
321
+ * stealth runtime re-checks per request (a fetch may override the browser); this
322
+ * rule fails the declaration instead of the first challenged request.
323
+ */
324
+ function validateResolverClientProfileDeclaration(
325
+ provider: ProviderDeclarationRulesInput,
326
+ violations: DeclarationViolation[],
327
+ ): void {
328
+ const resolver = provider.resolver;
329
+ if (
330
+ !resolver ||
331
+ !Array.isArray(resolver.kinds) ||
332
+ !resolver.kinds.some(kindRequiresClientProfile) ||
333
+ typeof resolver.clientProfile !== "string"
334
+ ) {
335
+ return;
336
+ }
337
+ let sessionBrowser: string;
338
+ try {
339
+ sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
340
+ } catch {
341
+ // An unsupported stealth selection is reported by the stealth runtime itself.
342
+ return;
343
+ }
344
+ const family = resolverClientProfileFamily(resolver.clientProfile);
345
+ if (family === sessionBrowser) return;
346
+ violations.push({
347
+ ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
348
+ path: "resolver.clientProfile",
349
+ message: family
350
+ ? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
351
+ : `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
352
+ fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
353
+ });
354
+ }
355
+
310
356
  function declaredProxyVendors(policy: ProviderProxyPolicy): ProviderProxyProvider[] {
311
357
  const vendors = [...(policy.providers ?? [])];
312
358
  if (policy.provider !== undefined) vendors.push(policy.provider);
package/src/define.ts CHANGED
@@ -6,8 +6,15 @@ import {
6
6
  } from "./declaration-validation.js";
7
7
  import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
8
8
  import { ProviderError, ValidationError } from "./errors.js";
9
- import { isEngineOwnedProxyCredentialName, isEngineOwnedTelemetryEnvName } from "./engine.js";
9
+ import {
10
+ engineOwnedResolverCredentialTarget,
11
+ isEngineOwnedEnvName,
12
+ isEngineOwnedProxyCredentialName,
13
+ isEngineOwnedRuntimeEnvName,
14
+ isEngineOwnedTelemetryEnvName,
15
+ } from "./engine.js";
10
16
  import { HealthScenarioSchema } from "./health-scenario.js";
17
+ import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
11
18
  import {
12
19
  NativeEgressPolicyValidationError,
13
20
  validateNativeProviderConfig,
@@ -49,9 +56,9 @@ import type {
49
56
  ProviderSecretDeclaration,
50
57
  ProviderStreamEvent,
51
58
  ProviderSttConfig,
59
+ ProviderStealthConfig,
52
60
  SchemaLike,
53
61
  SmsOtpMatcherDefinition,
54
- StealthProfileSelection,
55
62
  } from "./types.js";
56
63
  import {
57
64
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
@@ -151,6 +158,7 @@ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderRe
151
158
  "capsolver",
152
159
  "capmonster",
153
160
  "2captcha",
161
+ "hypersolutions",
154
162
  "custom",
155
163
  ] as const);
156
164
  export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
@@ -162,6 +170,7 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderCha
162
170
  "aws_waf",
163
171
  "akamai_sec_cpt",
164
172
  "akamai_sensor",
173
+ "akamai_sbsd",
165
174
  ] as const);
166
175
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
167
176
  const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
@@ -584,7 +593,7 @@ export interface ProviderDeclaration {
584
593
  /** Declares upstream host policy; this does not add a `ctx.allowedHosts` member. */
585
594
  allowedHosts?: string[];
586
595
  native?: NativeProviderConfig;
587
- stealth?: StealthProfileSelection;
596
+ stealth?: ProviderStealthConfig;
588
597
  /** Declares proxy policy; this is provider intent and does not add a `ctx.proxy` member. */
589
598
  proxy?: ProviderProxyConfig;
590
599
  ocr?: ProviderOcrConfig;
@@ -939,6 +948,15 @@ function validateProviderProxy(config: {
939
948
  secrets?: ProviderSecretDeclaration[];
940
949
  }): void {
941
950
  for (const secret of config.secrets ?? []) {
951
+ const engineName = engineOwnedResolverCredentialTarget(secret.name);
952
+ if (engineName !== undefined) {
953
+ throw new ValidationError(
954
+ `Provider "${config.id}" cannot declare engine-owned resolver credential "${secret.name}"`,
955
+ {
956
+ fix: `Remove "${secret.name}" from provider secrets; configure the solver key only on the provider engine as ${engineName}.`,
957
+ },
958
+ );
959
+ }
942
960
  if (isEngineOwnedTelemetryEnvName(secret.name)) {
943
961
  throw new ValidationError(
944
962
  `Provider "${config.id}" cannot declare engine-owned telemetry variable "${secret.name}"`,
@@ -947,6 +965,22 @@ function validateProviderProxy(config: {
947
965
  },
948
966
  );
949
967
  }
968
+ if (isEngineOwnedRuntimeEnvName(secret.name)) {
969
+ throw new ValidationError(
970
+ `Provider "${config.id}" cannot declare engine-owned runtime variable "${secret.name}"`,
971
+ {
972
+ fix: `Remove "${secret.name}" from provider secrets; the provider engine reads it from its own environment.`,
973
+ },
974
+ );
975
+ }
976
+ if (isEngineOwnedEnvName(secret.name) && !isEngineOwnedProxyCredentialName(secret.name)) {
977
+ throw new ValidationError(
978
+ `Provider "${config.id}" cannot declare engine-owned control variable "${secret.name}"`,
979
+ {
980
+ fix: `Remove "${secret.name}" from provider secrets; configure it only on the provider engine.`,
981
+ },
982
+ );
983
+ }
950
984
  if (!isEngineOwnedProxyCredentialName(secret.name)) continue;
951
985
  throw new ValidationError(
952
986
  `Provider "${config.id}" cannot declare engine-owned proxy credential "${secret.name}"`,
@@ -1163,6 +1197,14 @@ function validateProviderResolver(config: { id: string; resolver?: ProviderResol
1163
1197
  },
1164
1198
  );
1165
1199
  }
1200
+ if (resolver.kinds.some(kindRequiresClientProfile) && resolver.clientProfile === undefined) {
1201
+ throw new ValidationError(
1202
+ `Provider "${config.id}" must declare resolver.clientProfile for akamai_sbsd.`,
1203
+ {
1204
+ fix: `Set resolver.clientProfile for provider "${config.id}" to the transport-owned profile used by its Akamai session.`,
1205
+ },
1206
+ );
1207
+ }
1166
1208
  }
1167
1209
 
1168
1210
  function validateResolverLiteralArray<TValue extends string>(
package/src/engine.ts CHANGED
@@ -1,4 +1,11 @@
1
+ import { readDiagnosticEnv } from "./runtime/diagnostic-env.js";
1
2
  import { ProviderError } from "./errors.js";
3
+ import {
4
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
5
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
6
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
7
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
8
+ } from "./runtime/resolver-config.js";
2
9
  import {
3
10
  OTEL_EXPORTER_OTLP_ENDPOINT,
4
11
  OTEL_EXPORTER_OTLP_HEADERS,
@@ -55,6 +62,36 @@ export function isEngineOwnedProxyCredentialName(name: string): boolean {
55
62
  return ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAME_SET.has(canonicalEnvName(name));
56
63
  }
57
64
 
65
+ /** Hosted resolver credentials owned by the engine and never projected to providers. */
66
+ export const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES = [
67
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
68
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
69
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
70
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
71
+ ] as const;
72
+
73
+ const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET = new Set<string>(
74
+ ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
75
+ );
76
+
77
+ /**
78
+ * Engine variable a provider-declared secret name resolves to, or `undefined` when the
79
+ * name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
80
+ * (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
81
+ */
82
+ export function engineOwnedResolverCredentialTarget(name: string): string | undefined {
83
+ const canonical = canonicalEnvName(name);
84
+ if (ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET.has(canonical)) return canonical;
85
+ if (/^APIFUSE__PROVIDER__.+__HYPER_API_KEY$/u.test(canonical)) {
86
+ return APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ export function isEngineOwnedResolverCredentialName(name: string): boolean {
92
+ return engineOwnedResolverCredentialTarget(name) !== undefined;
93
+ }
94
+
58
95
  /**
59
96
  * Trace-export configuration owned by the engine and forbidden in provider
60
97
  * declarations. The header variables carry collector credentials; the rest are
@@ -75,9 +112,47 @@ export function isEngineOwnedTelemetryEnvName(name: string): boolean {
75
112
  return ENGINE_OWNED_TELEMETRY_ENV_NAME_SET.has(canonicalEnvName(name));
76
113
  }
77
114
 
115
+ /** Engine control-plane configuration is never declarable or visible to provider code. */
116
+ function isEngineOwnedEngineEnvName(name: string): boolean {
117
+ return canonicalEnvName(name).startsWith("APIFUSE__ENGINE__");
118
+ }
119
+
120
+ /**
121
+ * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
122
+ * tokens, and the CDP pool. Providers never own them: declarations are rejected
123
+ * and projections drop them, like the vendor credentials above.
124
+ */
125
+ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
126
+ "APIFUSE__STT__CLOUDFLARE_API_TOKEN",
127
+ "APIFUSE__OCR__CLOUDFLARE_API_TOKEN",
128
+ "APIFUSE__OCR__API_KEY",
129
+ "APIFUSE__CLOUDFLARE__ACCOUNT_ID",
130
+ "APIFUSE__CACHE__KEY_PEPPER",
131
+ "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
132
+ ] as const;
133
+
134
+ const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
135
+
136
+ /** CDP pool settings form a prefix family (`APIFUSE__CDP_POOL__URL` today); the whole prefix is engine-owned. */
137
+ const ENGINE_OWNED_CDP_POOL_ENV_PREFIX = "APIFUSE__CDP_POOL__";
138
+
139
+ export function isEngineOwnedRuntimeEnvName(name: string): boolean {
140
+ const canonical = canonicalEnvName(name);
141
+ return (
142
+ ENGINE_OWNED_RUNTIME_ENV_NAME_SET.has(canonical) ||
143
+ canonical.startsWith(ENGINE_OWNED_CDP_POOL_ENV_PREFIX)
144
+ );
145
+ }
146
+
78
147
  /** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
79
148
  export function isEngineOwnedEnvName(name: string): boolean {
80
- return isEngineOwnedProxyCredentialName(name) || isEngineOwnedTelemetryEnvName(name);
149
+ return (
150
+ isEngineOwnedEngineEnvName(name) ||
151
+ isEngineOwnedProxyCredentialName(name) ||
152
+ isEngineOwnedResolverCredentialName(name) ||
153
+ isEngineOwnedRuntimeEnvName(name) ||
154
+ isEngineOwnedTelemetryEnvName(name)
155
+ );
81
156
  }
82
157
 
83
158
  /** Capture credentials in the engine host before constructing provider bindings. */
@@ -86,7 +161,7 @@ export function readEngineProxyCredentials(
86
161
  ): Readonly<Record<string, string>> {
87
162
  return Object.fromEntries(
88
163
  ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES.flatMap((name) => {
89
- const value = environment[name]?.trim();
164
+ const value = readDiagnosticEnv(name, environment)?.trim();
90
165
  return value ? [[name, value] as const] : [];
91
166
  }),
92
167
  );
@@ -100,7 +175,7 @@ export function createProviderEnvironment(
100
175
  return Object.fromEntries(
101
176
  declaredNames.flatMap((name) => {
102
177
  if (isEngineOwnedEnvName(name)) return [];
103
- const value = environment[name];
178
+ const value = readDiagnosticEnv(name, environment);
104
179
  return value === undefined ? [] : [[name, value] as const];
105
180
  }),
106
181
  );
@@ -10,6 +10,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
10
10
  "BROWSER_CDP_POOL_REQUIRED",
11
11
  "BROWSER_RUNTIME_UNSUPPORTED",
12
12
  "STEALTH_RUNTIME_UNSUPPORTED",
13
+ "STEALTH_HEADER_OVERRIDE_UNSUPPORTED",
13
14
  "SSE_EVENT_UNDECLARED",
14
15
  "STREAM_EVENT_TOO_LARGE",
15
16
  "STREAM_CHUNK_TOO_LARGE",
@@ -51,6 +52,13 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
51
52
  "http_redirect_missing_location",
52
53
  "http_redirect_loop",
53
54
  "transport_invalid_url",
55
+ "EGRESS_LEASE_INVALID",
56
+ "EGRESS_LEASE_BINDING_INVALID",
57
+ "EGRESS_LEASE_KEY_MISSING",
58
+ "EGRESS_LEASE_KEY_WEAK",
59
+ "STEALTH_BODY_UNSUPPORTED",
60
+ "REPLAY_SESSION_MISMATCH",
61
+ "REPLAY_ALREADY_ATTEMPTED",
54
62
  "retry_exhausted",
55
63
  "auth_abort_unsafe_data",
56
64
  "credentials_auth_missing_credential_keys",
@@ -117,6 +125,11 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, Provide
117
125
  // contract): the upstream evaluated the request and said no under its
118
126
  // own rules — a conflict with upstream state, never a 5xx.
119
127
  ["UPSTREAM_REJECTED", 409],
128
+ // Only an unverifiable or foreign handle is the caller's: key and binding
129
+ // faults stay unmapped (500) because the engine host, not the caller, owns them.
130
+ ["EGRESS_LEASE_INVALID", 409],
131
+ ["REPLAY_SESSION_MISMATCH", 409],
132
+ ["REPLAY_ALREADY_ATTEMPTED", 409],
120
133
  ["UPSTREAM_ERROR", 502],
121
134
  ["BLOCKED", 502],
122
135
  ["OCR_UNAVAILABLE", 503],
package/src/index.ts CHANGED
@@ -198,17 +198,43 @@ export {
198
198
  type TelemetryKey,
199
199
  type TenantNeutral,
200
200
  } from "./runtime/request-telemetry.js";
201
+ export { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "./runtime/egress-lease.js";
202
+ export {
203
+ ResolverTelemetryCollector,
204
+ type ResolverCacheReadTelemetryEvent,
205
+ type ResolverCacheWriteTelemetryEvent,
206
+ type ResolverFailoverTelemetryEvent,
207
+ type ResolverIdentityTelemetryEvent,
208
+ type ResolverOutcomeTelemetryEvent,
209
+ type ResolverTelemetryAttemptOutcome,
210
+ type ResolverAttemptSample,
211
+ type ResolverTelemetryCacheStatus,
212
+ type ResolverTelemetryCacheWriteReason,
213
+ type ResolverTelemetryErrorClass,
214
+ type ResolverTelemetryHeaderPayload,
215
+ type ResolverTelemetryIdentitySource,
216
+ type ResolverTelemetryLogPayload,
217
+ type ResolverTelemetryOutcome,
218
+ type ResolverTelemetryPhase,
219
+ type ResolverTelemetrySink,
220
+ type ResolverVendorAttemptTelemetryEvent,
221
+ } from "./runtime/resolver-telemetry.js";
201
222
  export {
202
223
  APIFUSE__CDP_POOL__URL,
203
224
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
204
225
  APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
205
226
  APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
227
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
206
228
  APIFUSE__RESOLVER__TIMEOUT_MS,
207
229
  DEFAULT_RESOLVER_TIMEOUT_MS,
208
230
  } from "./runtime/resolver-config.js";
209
231
  export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
210
232
  export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
211
- export type { ResolverVendorTransport } from "./runtime/resolver-vendors/types.js";
233
+ export type {
234
+ ResolverPaidUsageContext,
235
+ ResolverVendorTransport,
236
+ ResolverVendorUnavailableReason,
237
+ } from "./runtime/resolver-vendors/types.js";
212
238
  export {
213
239
  assertRequiredSecretsPresent,
214
240
  listMissingRequiredSecrets,
@@ -249,7 +275,10 @@ export {
249
275
  type Span,
250
276
  type TraceContext,
251
277
  } from "./runtime/trace.js";
278
+ export type { DiagnosticRedactor } from "./runtime/diagnostic-redactor.js";
252
279
  export {
280
+ APIFUSE_CONTENT_PROVENANCE_META_KEY,
281
+ APIFUSE_CONTENT_TRUST_META_KEY,
253
282
  APIFUSE_DESCRIPTION_KEY_META_KEY,
254
283
  APIFUSE_REDACTION_MARKER,
255
284
  APIFUSE_SENSITIVE_KIND_META_KEY,
@@ -259,11 +288,13 @@ export {
259
288
  field,
260
289
  fields,
261
290
  isSensitiveSchema,
291
+ publicField,
262
292
  redactPayload,
263
293
  type SensitiveFieldKind,
264
294
  type SensitiveFieldOptions,
265
295
  type SensitivePath,
266
296
  sensitive,
297
+ untrustedContent,
267
298
  z,
268
299
  } from "./schema.js";
269
300
  export {
@@ -432,6 +463,7 @@ export type {
432
463
  ProviderRuntimeTarget,
433
464
  ProviderRuntimeState,
434
465
  ProviderSecretDeclaration,
466
+ ProviderStealthConfig,
435
467
  ProviderStateDurationString,
436
468
  ProviderStateNamespace,
437
469
  ProviderStreamEvent,
@@ -455,6 +487,7 @@ export type {
455
487
  StateWriteOptions,
456
488
  StealthClient,
457
489
  StealthBrowser,
490
+ StealthChallengeClassification,
458
491
  StealthCookieStore,
459
492
  StealthCookieStoreV1,
460
493
  StealthFetchOptions,