@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.5

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 (165) hide show
  1. package/AUTHORING.md +53 -0
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-check.ts +18 -58
  4. package/bin/apifuse-create.ts +1 -1
  5. package/bin/apifuse-dev.ts +27 -52
  6. package/bin/apifuse-pack-smoke.ts +22 -81
  7. package/bin/apifuse-pack-types.ts +266 -0
  8. package/bin/apifuse-perf.ts +45 -127
  9. package/bin/apifuse-record.ts +23 -61
  10. package/bin/apifuse-submit-check.ts +266 -34
  11. package/bin/apifuse.ts +1 -1
  12. package/bin/submit-check-xml.ts +1 -1
  13. package/dist/auth-turn/index.d.ts +1 -1
  14. package/dist/auth.d.ts +2 -2
  15. package/dist/auth.js +9 -18
  16. package/dist/ceremonies/index.d.ts +1 -1
  17. package/dist/ceremonies/index.js +8 -18
  18. package/dist/config/loader.d.ts +2 -2
  19. package/dist/config/loader.js +19 -42
  20. package/dist/contract-serialization.d.ts +2 -2
  21. package/dist/contract-serialization.js +3 -6
  22. package/dist/contract-types.d.ts +2 -2
  23. package/dist/contract.d.ts +3 -3
  24. package/dist/contract.js +4 -6
  25. package/dist/define.d.ts +8 -1
  26. package/dist/define.js +57 -115
  27. package/dist/dev.d.ts +1 -1
  28. package/dist/dev.js +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.js +1 -3
  31. package/dist/i18n/catalog.d.ts +2 -2
  32. package/dist/i18n/catalog.js +4 -10
  33. package/dist/i18n/index.d.ts +2 -2
  34. package/dist/i18n/index.js +2 -2
  35. package/dist/i18n/keys.d.ts +2 -2
  36. package/dist/index.d.ts +43 -42
  37. package/dist/index.js +38 -37
  38. package/dist/lint.js +8 -15
  39. package/dist/provider.d.ts +11 -11
  40. package/dist/provider.js +9 -9
  41. package/dist/public-schema-field-lint.d.ts +1 -1
  42. package/dist/recipes/gov-api.js +1 -1
  43. package/dist/runtime/auth-flow.d.ts +1 -1
  44. package/dist/runtime/auth-flow.js +3 -3
  45. package/dist/runtime/browser.d.ts +1 -1
  46. package/dist/runtime/browser.js +15 -29
  47. package/dist/runtime/cache.d.ts +1 -1
  48. package/dist/runtime/cache.js +4 -8
  49. package/dist/runtime/choice.d.ts +1 -1
  50. package/dist/runtime/choice.js +12 -34
  51. package/dist/runtime/credential.d.ts +1 -1
  52. package/dist/runtime/credential.js +1 -1
  53. package/dist/runtime/env.d.ts +1 -1
  54. package/dist/runtime/executor.d.ts +1 -1
  55. package/dist/runtime/executor.js +2 -2
  56. package/dist/runtime/http.d.ts +2 -2
  57. package/dist/runtime/http.js +5 -5
  58. package/dist/runtime/insights.d.ts +1 -1
  59. package/dist/runtime/insights.js +6 -13
  60. package/dist/runtime/instrumentation.d.ts +2 -2
  61. package/dist/runtime/instrumentation.js +5 -15
  62. package/dist/runtime/keyring.js +1 -1
  63. package/dist/runtime/namespace.js +1 -1
  64. package/dist/runtime/otlp.d.ts +1 -1
  65. package/dist/runtime/perf.d.ts +1 -1
  66. package/dist/runtime/provider.d.ts +1 -1
  67. package/dist/runtime/provider.js +1 -2
  68. package/dist/runtime/proxy-errors.d.ts +1 -1
  69. package/dist/runtime/proxy-errors.js +3 -5
  70. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  71. package/dist/runtime/proxy-retry-policy.js +2 -2
  72. package/dist/runtime/proxy-telemetry.d.ts +1 -1
  73. package/dist/runtime/proxy-telemetry.js +16 -48
  74. package/dist/runtime/redis.d.ts +1 -1
  75. package/dist/runtime/redis.js +2 -4
  76. package/dist/runtime/request-options.d.ts +1 -1
  77. package/dist/runtime/state.d.ts +2 -2
  78. package/dist/runtime/state.js +3 -3
  79. package/dist/runtime/stealth.d.ts +2 -2
  80. package/dist/runtime/stealth.js +6 -6
  81. package/dist/runtime/stt.d.ts +1 -1
  82. package/dist/runtime/stt.js +11 -15
  83. package/dist/runtime/trace.d.ts +2 -2
  84. package/dist/runtime/trace.js +2 -4
  85. package/dist/runtime/waterfall.d.ts +1 -1
  86. package/dist/schema.d.ts +1 -1
  87. package/dist/schema.js +7 -15
  88. package/dist/serve.d.ts +1 -1
  89. package/dist/serve.js +1 -1
  90. package/dist/server/index.d.ts +7 -7
  91. package/dist/server/index.js +6 -6
  92. package/dist/server/self-test-redaction.d.ts +1 -1
  93. package/dist/server/self-test-redaction.js +1 -1
  94. package/dist/server/self-test.d.ts +104 -3
  95. package/dist/server/self-test.js +673 -115
  96. package/dist/server/serve.d.ts +3 -3
  97. package/dist/server/serve.js +37 -49
  98. package/dist/server/types.js +2 -7
  99. package/dist/stealth/profiles.d.ts +1 -1
  100. package/dist/stealth/profiles.js +5 -14
  101. package/dist/stream.d.ts +1 -1
  102. package/dist/testing/index.d.ts +2 -2
  103. package/dist/testing/index.js +2 -2
  104. package/dist/testing/run.d.ts +1 -1
  105. package/dist/testing/run.js +7 -14
  106. package/dist/types.d.ts +57 -0
  107. package/dist/user-input.d.ts +30 -0
  108. package/dist/user-input.js +66 -0
  109. package/package.json +6 -4
  110. package/src/auth-turn/index.ts +1 -1
  111. package/src/auth.ts +28 -86
  112. package/src/ceremonies/index.ts +24 -78
  113. package/src/config/loader.ts +58 -176
  114. package/src/contract-serialization.ts +5 -11
  115. package/src/contract-types.ts +2 -2
  116. package/src/contract.ts +12 -28
  117. package/src/define.ts +174 -495
  118. package/src/dev.ts +4 -9
  119. package/src/errors.ts +4 -11
  120. package/src/i18n/catalog.ts +10 -32
  121. package/src/i18n/index.ts +2 -2
  122. package/src/i18n/keys.ts +5 -11
  123. package/src/index.ts +44 -42
  124. package/src/lint.ts +89 -159
  125. package/src/provider.ts +12 -11
  126. package/src/public-schema-field-lint.ts +7 -33
  127. package/src/recipes/gov-api.ts +2 -5
  128. package/src/runtime/auth-flow.ts +5 -7
  129. package/src/runtime/browser.ts +64 -187
  130. package/src/runtime/cache.ts +20 -67
  131. package/src/runtime/choice.ts +55 -129
  132. package/src/runtime/credential.ts +2 -2
  133. package/src/runtime/env.ts +1 -1
  134. package/src/runtime/executor.ts +9 -19
  135. package/src/runtime/http.ts +7 -7
  136. package/src/runtime/insights.ts +15 -53
  137. package/src/runtime/instrumentation.ts +19 -58
  138. package/src/runtime/keyring.ts +7 -19
  139. package/src/runtime/namespace.ts +2 -7
  140. package/src/runtime/otlp.ts +12 -23
  141. package/src/runtime/perf.ts +1 -1
  142. package/src/runtime/provider.ts +4 -9
  143. package/src/runtime/proxy-errors.ts +17 -38
  144. package/src/runtime/proxy-retry-policy.ts +3 -3
  145. package/src/runtime/proxy-telemetry.ts +23 -72
  146. package/src/runtime/redis.ts +4 -12
  147. package/src/runtime/request-options.ts +4 -13
  148. package/src/runtime/state.ts +30 -109
  149. package/src/runtime/stealth.ts +8 -8
  150. package/src/runtime/stt.ts +38 -94
  151. package/src/runtime/trace.ts +14 -44
  152. package/src/runtime/waterfall.ts +5 -18
  153. package/src/schema.ts +23 -84
  154. package/src/serve.ts +1 -1
  155. package/src/server/index.ts +12 -7
  156. package/src/server/self-test-redaction.ts +2 -2
  157. package/src/server/self-test.ts +857 -132
  158. package/src/server/serve.ts +89 -238
  159. package/src/server/types.ts +6 -19
  160. package/src/stealth/profiles.ts +10 -26
  161. package/src/stream.ts +8 -19
  162. package/src/testing/index.ts +2 -2
  163. package/src/testing/run.ts +19 -67
  164. package/src/types.ts +58 -0
  165. package/src/user-input.ts +118 -0
@@ -2,9 +2,9 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
- import Redis from "ioredis";
5
+ import { Redis } from "ioredis";
6
6
 
7
- import type { ProviderProxyPolicy, TraceConfig } from "../types";
7
+ import type { ProviderProxyPolicy, TraceConfig } from "../types.js";
8
8
 
9
9
  export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
10
10
  export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
@@ -12,12 +12,9 @@ export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
12
12
  export const SMARTPROXY_MAX_POOL_SIZE = 20;
13
13
  export const DEFAULT_PROXY_PROVIDER_ENV = "APIFUSE__PROXY__PROVIDER";
14
14
  export const DEFAULT_PROXY_COUNTRY_ENV = "APIFUSE__PROXY__DEFAULT_COUNTRY";
15
- export const DEFAULT_PROXY_LIFETIME_ENV =
16
- "APIFUSE__PROXY__DEFAULT_LIFETIME_MINUTES";
17
- export const PROVIDER_CACHE_REDIS_URL_ENV =
18
- "APIFUSE__PROVIDER__CACHE_REDIS_URL";
19
- export const PROVIDER_STATE_REDIS_URL_ENV =
20
- "APIFUSE__PROVIDER__STATE_REDIS_URL";
15
+ export const DEFAULT_PROXY_LIFETIME_ENV = "APIFUSE__PROXY__DEFAULT_LIFETIME_MINUTES";
16
+ export const PROVIDER_CACHE_REDIS_URL_ENV = "APIFUSE__PROVIDER__CACHE_REDIS_URL";
17
+ export const PROVIDER_STATE_REDIS_URL_ENV = "APIFUSE__PROVIDER__STATE_REDIS_URL";
21
18
  export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
22
19
 
23
20
  export type ProxyOptions = {
@@ -165,8 +162,7 @@ const SMARTPROXY_LOCK_POLL_MAX_MS = 9_000;
165
162
  const SMARTPROXY_DEADLINE_MARGIN_MS = 1_000;
166
163
  const SMARTPROXY_INVALIDATION_SKIP_REDIS_MS = 30_000;
167
164
  const SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS = 3;
168
- const SMARTPROXY_ALLOCATOR_DEADLINE_MS =
169
- SMARTPROXY_LOCK_TTL_MS - SMARTPROXY_DEADLINE_MARGIN_MS;
165
+ const SMARTPROXY_ALLOCATOR_DEADLINE_MS = SMARTPROXY_LOCK_TTL_MS - SMARTPROXY_DEADLINE_MARGIN_MS;
170
166
  const SMARTPROXY_ALLOCATOR_RETRY_BASE_MS = 25;
171
167
  // Smartproxy API extraction returns fresh IP:port candidates; the `life`
172
168
  // parameter controls session duration intent, not a hard endpoint lease. Keep
@@ -197,15 +193,11 @@ export function providerStateRedisUrlFromEnv(): string | undefined {
197
193
  }
198
194
 
199
195
  /** @internal Test-only hook for exercising shared proxy-cache behavior. */
200
- export function __setProxyRedisForTests(
201
- redis: ProxyRedisClient | undefined,
202
- ): void {
196
+ export function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void {
203
197
  proxyRedisForTests = redis;
204
198
  }
205
199
 
206
- export function __setSmartproxyAllocatorDeadlineMsForTests(
207
- deadlineMs: number | undefined,
208
- ): void {
200
+ export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void {
209
201
  smartproxyAllocatorDeadlineMsForTests = deadlineMs;
210
202
  }
211
203
 
@@ -230,9 +222,7 @@ function getProxyRedis(): ProxyRedisClient | undefined {
230
222
  return redis;
231
223
  }
232
224
 
233
- async function withRedisTimeout<T>(
234
- operation: () => Promise<T>,
235
- ): Promise<T | undefined> {
225
+ async function withRedisTimeout<T>(operation: () => Promise<T>): Promise<T | undefined> {
236
226
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
237
227
  try {
238
228
  const timeout = new Promise<undefined>((resolve) => {
@@ -318,17 +308,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
318
308
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
319
309
  }
320
310
 
321
- function toProxyDiagnostics(
322
- value: unknown,
323
- ): Record<string, string | number | boolean> | undefined {
311
+ function toProxyDiagnostics(value: unknown): Record<string, string | number | boolean> | undefined {
324
312
  if (!isRecord(value)) return undefined;
325
313
  const diagnostics: Record<string, string | number | boolean> = {};
326
314
  for (const [key, item] of Object.entries(value)) {
327
- if (
328
- typeof item === "string" ||
329
- typeof item === "number" ||
330
- typeof item === "boolean"
331
- ) {
315
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
332
316
  diagnostics[key] = item;
333
317
  }
334
318
  }
@@ -411,11 +395,8 @@ function applyStickyProxySession(proxyUrl: string): string {
411
395
  }
412
396
 
413
397
  const username = decodeURIComponent(parsed.username);
414
- const sessionId =
415
- process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
416
- const sessionDuration = readPositiveIntegerEnv(
417
- "APIFUSE__PROXY__SESSION_DURATION",
418
- );
398
+ const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
399
+ const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
419
400
  const stickyUsername = host.includes("smartproxy")
420
401
  ? buildSmartproxyUsername(username, sessionId, sessionDuration)
421
402
  : buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
@@ -430,27 +411,16 @@ function buildSmartproxyUsername(
430
411
  sessionDuration?: string,
431
412
  ): string {
432
413
  const parts = username.split("_");
433
- const configuredLife = parts
434
- .find((part) => part.startsWith("life-"))
435
- ?.slice("life-".length);
414
+ const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
436
415
  const baseUsername = parts
437
416
  .filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
438
417
  .join("_");
439
418
  return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
440
419
  }
441
420
 
442
- function buildDecodoUsername(
443
- username: string,
444
- sessionId: string,
445
- sessionDuration: string,
446
- ): string {
447
- const withoutSticky = username.replace(
448
- /-session-.+-sessionduration-\d+$/,
449
- "",
450
- );
451
- const baseUsername = withoutSticky.startsWith("user-")
452
- ? withoutSticky
453
- : `user-${withoutSticky}`;
421
+ function buildDecodoUsername(username: string, sessionId: string, sessionDuration: string): string {
422
+ const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
423
+ const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
454
424
  return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
455
425
  }
456
426
 
@@ -461,9 +431,7 @@ function syncProxyEnv(config: ApiFuseConfig): void {
461
431
  }
462
432
  }
463
433
 
464
- export function resolveProxyConfig(
465
- options: ProxyResolutionOptions = {},
466
- ): ResolvedProxyConfig {
434
+ export function resolveProxyConfig(options: ProxyResolutionOptions = {}): ResolvedProxyConfig {
467
435
  const explicitProxyUrl = normalizeProxyUrl(options.proxy);
468
436
  if (explicitProxyUrl) {
469
437
  return { shouldWarn: false, url: explicitProxyUrl };
@@ -485,9 +453,7 @@ export function resolveProxyConfig(
485
453
  return { shouldWarn: false, url: envProxyUrl };
486
454
  }
487
455
 
488
- const configuredProxyUrl = normalizeProxyUrl(
489
- options.apifuseConfig?.proxy?.url,
490
- );
456
+ const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
491
457
  if (configuredProxyUrl) {
492
458
  return { shouldWarn: false, url: configuredProxyUrl };
493
459
  }
@@ -539,10 +505,7 @@ export async function resolveProxyConfigAsync(
539
505
  options.affinityKey,
540
506
  );
541
507
  options.telemetry?.recordProxyResolution(allocated.telemetry);
542
- const poolIndex = selectProxyPoolIndex(
543
- allocated.pool.urls.length,
544
- options.proxyAttempt,
545
- );
508
+ const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, options.proxyAttempt);
546
509
  return {
547
510
  shouldWarn: false,
548
511
  url: allocated.pool.urls[poolIndex],
@@ -570,9 +533,7 @@ export async function resolveProxyConfigAsync(
570
533
  }
571
534
  }
572
535
 
573
- function resolvePolicy(
574
- options: ProxyResolutionOptions,
575
- ): ProviderProxyPolicy | undefined {
536
+ function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | undefined {
576
537
  if (options.proxyPolicy) {
577
538
  return options.proxyPolicy;
578
539
  }
@@ -585,30 +546,20 @@ function resolvePolicy(
585
546
 
586
547
  function resolveProxyProvider(policy: ProviderProxyPolicy): string {
587
548
  return (
588
- policy.provider ??
589
- process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ??
590
- "custom"
549
+ policy.provider ?? process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ?? "custom"
591
550
  );
592
551
  }
593
552
 
594
- function resolveSmartproxyCountry(
595
- policy: ProviderProxyPolicy,
596
- ): string | undefined {
553
+ function resolveSmartproxyCountry(policy: ProviderProxyPolicy): string | undefined {
597
554
  return (
598
- policy.geo?.country ??
599
- process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ??
600
- undefined
555
+ policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined
601
556
  );
602
557
  }
603
558
 
604
559
  function resolveSmartproxyLifetime(policy: ProviderProxyPolicy): number {
605
560
  const configuredLifetime =
606
- policy.session?.lifetimeMinutes ??
607
- readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
608
- return Math.min(
609
- SMARTPROXY_MAX_LIFETIME_MINUTES,
610
- Math.max(1, Math.floor(configuredLifetime)),
611
- );
561
+ policy.session?.lifetimeMinutes ?? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
562
+ return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
612
563
  }
613
564
 
614
565
  function readPositiveNumberEnv(name: string, fallback: number): number {
@@ -624,10 +575,7 @@ function readPositiveNumberEnv(name: string, fallback: number): number {
624
575
  function resolveSmartproxyPoolSize(policy: ProviderProxyPolicy): number {
625
576
  return Math.min(
626
577
  SMARTPROXY_MAX_POOL_SIZE,
627
- Math.max(
628
- 1,
629
- Math.floor(policy.session?.poolSize ?? DEFAULT_SMARTPROXY_POOL_SIZE),
630
- ),
578
+ Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_SMARTPROXY_POOL_SIZE)),
631
579
  );
632
580
  }
633
581
 
@@ -635,9 +583,7 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
635
583
  if (poolSize <= 1) {
636
584
  return 0;
637
585
  }
638
- const normalizedAttempt = Number.isFinite(attempt)
639
- ? Math.max(0, Math.floor(attempt))
640
- : 0;
586
+ const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
641
587
  return normalizedAttempt % poolSize;
642
588
  }
643
589
 
@@ -651,10 +597,7 @@ function buildSmartproxyCacheKey(
651
597
  provider: "smartproxy",
652
598
  country: resolveSmartproxyCountry(policy),
653
599
  affinity: policy.session?.affinity ?? "request",
654
- affinityKey:
655
- (policy.session?.affinity ?? "request") === "request"
656
- ? undefined
657
- : affinityKey,
600
+ affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
658
601
  lifetimeMinutes,
659
602
  poolSize,
660
603
  });
@@ -666,11 +609,7 @@ async function allocateSmartproxy(
666
609
  lifetimeMinutes: number,
667
610
  affinityKey: string | undefined,
668
611
  ): Promise<SmartproxyAllocationResult> {
669
- const cacheKey = buildSmartproxyCacheKey(
670
- policy,
671
- affinityKey,
672
- lifetimeMinutes,
673
- );
612
+ const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
674
613
  const startedAt = Date.now();
675
614
  const now = startedAt;
676
615
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
@@ -730,9 +669,7 @@ async function readSmartproxyRedisPool(
730
669
  const redis = getProxyRedis();
731
670
  if (!redis || !(await ensureRedisReady(redis))) return null;
732
671
  const redisStartedAt = Date.now();
733
- const raw = await withRedisTimeout(() =>
734
- redis.get(smartproxyRedisPoolKey(cacheKey)),
735
- );
672
+ const raw = await withRedisTimeout(() => redis.get(smartproxyRedisPoolKey(cacheKey)));
736
673
  const redisReadMs = Math.max(0, Date.now() - redisStartedAt);
737
674
  if (typeof raw !== "string") return null;
738
675
  const pool = safeParseSmartproxyPool(raw);
@@ -756,16 +693,9 @@ async function refreshSmartproxyPool(
756
693
  lifetimeMinutes: number,
757
694
  ): Promise<void> {
758
695
  try {
759
- await allocateSmartproxyShared(
760
- cacheKey,
761
- policy,
762
- appKey,
763
- lifetimeMinutes,
764
- Date.now(),
765
- {
766
- background: true,
767
- },
768
- );
696
+ await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
697
+ background: true,
698
+ });
769
699
  } catch {
770
700
  // Soft refresh is opportunistic; current fresh pool remains usable.
771
701
  }
@@ -809,9 +739,7 @@ async function allocateSmartproxyShared(
809
739
  lifetimeMinutes,
810
740
  startedAt,
811
741
  {
812
- cacheStatus: options.background
813
- ? "soft_stale_refresh"
814
- : "allocator",
742
+ cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
815
743
  redis,
816
744
  poolKey,
817
745
  },
@@ -838,15 +766,10 @@ async function allocateSmartproxyShared(
838
766
  if (typeof pttl === "number" && pttl <= 0) {
839
767
  continue;
840
768
  }
841
- if (
842
- Date.now() - startedAt >
843
- SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS
844
- ) {
769
+ if (Date.now() - startedAt > SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS) {
845
770
  break;
846
771
  }
847
- await sleep(
848
- Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)),
849
- );
772
+ await sleep(Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)));
850
773
  }
851
774
 
852
775
  throw new ProxyResolutionError(
@@ -893,9 +816,7 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
893
816
  }
894
817
 
895
818
  function smartproxyAllocatorDeadlineMs(): number {
896
- return (
897
- smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS
898
- );
819
+ return smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS;
899
820
  }
900
821
 
901
822
  function createDeadlineAbortController(deadlineAt: number): {
@@ -917,9 +838,7 @@ function createDeadlineAbortController(deadlineAt: number): {
917
838
  };
918
839
  }
919
840
 
920
- function smartproxyAllocatorDeadlineFailure(
921
- attempt: number,
922
- ): SmartproxyAllocatorFailure {
841
+ function smartproxyAllocatorDeadlineFailure(attempt: number): SmartproxyAllocatorFailure {
923
842
  return {
924
843
  ok: false,
925
844
  attempt,
@@ -981,22 +900,12 @@ async function allocateAndStoreSmartproxyPool(
981
900
  },
982
901
  ): Promise<SmartproxyAllocationResult> {
983
902
  const poolSize = resolveSmartproxyPoolSize(policy);
984
- const allocatorUrl = buildSmartproxyAllocatorUrl(
985
- policy,
986
- appKey,
987
- lifetimeMinutes,
988
- poolSize,
989
- );
903
+ const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
990
904
  const allocatorStartedAt = Date.now();
991
- const allocatorDeadlineAt =
992
- allocatorStartedAt + smartproxyAllocatorDeadlineMs();
905
+ const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
993
906
  let allocation: SmartproxyAllocatorSuccess | undefined;
994
907
  let lastFailure: SmartproxyAllocatorFailure | undefined;
995
- for (
996
- let attempt = 1;
997
- attempt <= SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
998
- attempt += 1
999
- ) {
908
+ for (let attempt = 1; attempt <= SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS; attempt += 1) {
1000
909
  if (Date.now() >= allocatorDeadlineAt) {
1001
910
  lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
1002
911
  break;
@@ -1013,8 +922,7 @@ async function allocateAndStoreSmartproxyPool(
1013
922
  lastFailure = attemptResult;
1014
923
  if (attempt < SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS) {
1015
924
  if (Date.now() >= allocatorDeadlineAt) break;
1016
- const { controller, dispose } =
1017
- createDeadlineAbortController(allocatorDeadlineAt);
925
+ const { controller, dispose } = createDeadlineAbortController(allocatorDeadlineAt);
1018
926
  try {
1019
927
  await sleep(smartproxyAllocatorBackoffMs(attempt), controller.signal);
1020
928
  } catch {
@@ -1027,9 +935,7 @@ async function allocateAndStoreSmartproxyPool(
1027
935
 
1028
936
  const allocatorMs = Math.max(0, Date.now() - allocatorStartedAt);
1029
937
  const allocatorAttempts =
1030
- allocation?.attempt ??
1031
- lastFailure?.attempt ??
1032
- SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
938
+ allocation?.attempt ?? lastFailure?.attempt ?? SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
1033
939
  const allocatorBodyClass =
1034
940
  lastFailure?.bodyClass ?? allocation?.bodyClass ?? "usable_proxy_endpoints";
1035
941
  const allocatorStatus = lastFailure ? lastFailure.status : allocation?.status;
@@ -1106,9 +1012,7 @@ async function allocateAndStoreSmartproxyPool(
1106
1012
  };
1107
1013
  }
1108
1014
 
1109
- type SmartproxyAllocatorAttemptResult =
1110
- | SmartproxyAllocatorSuccess
1111
- | SmartproxyAllocatorFailure;
1015
+ type SmartproxyAllocatorAttemptResult = SmartproxyAllocatorSuccess | SmartproxyAllocatorFailure;
1112
1016
 
1113
1017
  type SmartproxyAllocatorSuccess = {
1114
1018
  ok: true;
@@ -1150,10 +1054,7 @@ async function fetchSmartproxyAllocatorAttempt(
1150
1054
 
1151
1055
  let body: string;
1152
1056
  try {
1153
- body = await readSmartproxyAllocatorBodyWithDeadline(
1154
- response,
1155
- controller.signal,
1156
- );
1057
+ body = await readSmartproxyAllocatorBodyWithDeadline(response, controller.signal);
1157
1058
  } catch (error) {
1158
1059
  dispose();
1159
1060
  return {
@@ -1196,8 +1097,7 @@ async function fetchSmartproxyAllocatorAttempt(
1196
1097
  }
1197
1098
 
1198
1099
  function smartproxyAllocatorBackoffMs(attempt: number): number {
1199
- const base =
1200
- SMARTPROXY_ALLOCATOR_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1);
1100
+ const base = SMARTPROXY_ALLOCATOR_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1);
1201
1101
  const jitter = Math.floor(Math.random() * SMARTPROXY_ALLOCATOR_RETRY_BASE_MS);
1202
1102
  return base + jitter;
1203
1103
  }
@@ -1236,7 +1136,9 @@ function buildSmartproxyAllocatorUrl(
1236
1136
  if (country) {
1237
1137
  params.set("cc", country);
1238
1138
  }
1239
- return `https://www.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
1139
+ // www.smartproxy.org stopped serving the extraction API on 2026-07-21 (the
1140
+ // old path 404s into the marketing site); the API lives on the api host.
1141
+ return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
1240
1142
  }
1241
1143
 
1242
1144
  function parseSmartproxyAllocatorProxies(body: string): string[] {
@@ -1247,14 +1149,8 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
1247
1149
 
1248
1150
  try {
1249
1151
  const parsed: unknown = JSON.parse(trimmed);
1250
- const data =
1251
- parsed && typeof parsed === "object" && "data" in parsed
1252
- ? parsed.data
1253
- : undefined;
1254
- const list =
1255
- data && typeof data === "object" && "list" in data
1256
- ? data.list
1257
- : undefined;
1152
+ const data = parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : undefined;
1153
+ const list = data && typeof data === "object" && "list" in data ? data.list : undefined;
1258
1154
  if (Array.isArray(list)) {
1259
1155
  return list
1260
1156
  .map((item) => {
@@ -1263,8 +1159,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
1263
1159
  }
1264
1160
  const ip = "ip" in item && typeof item.ip === "string" ? item.ip : "";
1265
1161
  const port =
1266
- "port" in item &&
1267
- (typeof item.port === "string" || typeof item.port === "number")
1162
+ "port" in item && (typeof item.port === "string" || typeof item.port === "number")
1268
1163
  ? item.port
1269
1164
  : "";
1270
1165
  return ip && port ? `http://${ip}:${port}` : null;
@@ -1307,9 +1202,7 @@ export function clearProxyResolutionCache(): void {
1307
1202
  invalidatedProxyKeys.clear();
1308
1203
  }
1309
1204
 
1310
- function markSmartproxyCacheInvalidated(
1311
- options: ProxyResolutionOptions = {},
1312
- ): string | undefined {
1205
+ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): string | undefined {
1313
1206
  const policy = resolvePolicy(options);
1314
1207
  if (!policy || policy.mode === "disabled") {
1315
1208
  return undefined;
@@ -1319,23 +1212,14 @@ function markSmartproxyCacheInvalidated(
1319
1212
  }
1320
1213
 
1321
1214
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1322
- const cacheKey = buildSmartproxyCacheKey(
1323
- policy,
1324
- options.affinityKey,
1325
- lifetimeMinutes,
1326
- );
1327
- invalidatedProxyKeys.set(
1328
- cacheKey,
1329
- Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS,
1330
- );
1215
+ const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
1216
+ invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1331
1217
  proxyCache.delete(cacheKey);
1332
1218
  proxyInflight.delete(cacheKey);
1333
1219
  return cacheKey;
1334
1220
  }
1335
1221
 
1336
- export function invalidateProxyResolutionCache(
1337
- options: ProxyResolutionOptions = {},
1338
- ): boolean {
1222
+ export function invalidateProxyResolutionCache(options: ProxyResolutionOptions = {}): boolean {
1339
1223
  return markSmartproxyCacheInvalidated(options) !== undefined;
1340
1224
  }
1341
1225
 
@@ -1374,9 +1258,7 @@ async function importConfig(filePath: string): Promise<ApiFuseConfig | null> {
1374
1258
  }
1375
1259
  }
1376
1260
 
1377
- export async function loadApiFuseConfig(
1378
- dir: string = process.cwd(),
1379
- ): Promise<ApiFuseConfig> {
1261
+ export async function loadApiFuseConfig(dir: string = process.cwd()): Promise<ApiFuseConfig> {
1380
1262
  const tsPath = path.resolve(dir, "apifuse.config.ts");
1381
1263
  if (existsSync(tsPath)) {
1382
1264
  const config = await importConfig(tsPath);
@@ -6,8 +6,8 @@ import {
6
6
  isRecord,
7
7
  type JsonValue,
8
8
  toJsonValue,
9
- } from "./contract-json";
10
- import type { SchemaLike } from "./types";
9
+ } from "./contract-json.js";
10
+ import type { SchemaLike } from "./types.js";
11
11
 
12
12
  export function describeSchema(schema: SchemaLike): JsonValue {
13
13
  if (isZodSchema(schema)) {
@@ -17,10 +17,7 @@ export function describeSchema(schema: SchemaLike): JsonValue {
17
17
  vendor: "zod",
18
18
  typeName: getSchemaTypeName(schema),
19
19
  jsonSchema,
20
- jsonSchemaHash:
21
- jsonSchema === undefined
22
- ? undefined
23
- : digest(canonicalJson(jsonSchema)),
20
+ jsonSchemaHash: jsonSchema === undefined ? undefined : digest(canonicalJson(jsonSchema)),
24
21
  });
25
22
  }
26
23
  const standard = isRecord(schema) ? schema["~standard"] : undefined;
@@ -30,8 +27,7 @@ export function describeSchema(schema: SchemaLike): JsonValue {
30
27
  standard: "standard-schema-v1",
31
28
  vendor: typeof standard.vendor === "string" ? standard.vendor : "unknown",
32
29
  version:
33
- typeof standard.version === "number" ||
34
- typeof standard.version === "string"
30
+ typeof standard.version === "number" || typeof standard.version === "string"
35
31
  ? standard.version
36
32
  : undefined,
37
33
  });
@@ -43,9 +39,7 @@ export function describeSchema(schema: SchemaLike): JsonValue {
43
39
  });
44
40
  }
45
41
 
46
- export function serializeSmsMatcher(
47
- value: Record<string, unknown>,
48
- ): Record<string, unknown> {
42
+ export function serializeSmsMatcher(value: Record<string, unknown>): Record<string, unknown> {
49
43
  const code = value.code;
50
44
  if (!isRecord(code)) return value;
51
45
  const pattern = code.pattern;
@@ -1,5 +1,5 @@
1
- import type { JsonValue } from "./contract-json";
2
- import type { ProviderDefinition } from "./types";
1
+ import type { JsonValue } from "./contract-json.js";
2
+ import type { ProviderDefinition } from "./types.js";
3
3
 
4
4
  export const PROVIDER_CONTRACT_SCHEMA_VERSION = "2026-06-23";
5
5
 
package/src/contract.ts CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  type JsonPrimitive,
7
7
  type JsonValue,
8
8
  toJsonValue,
9
- } from "./contract-json";
10
- import { describeSchema, serializeSmsMatcher } from "./contract-serialization";
9
+ } from "./contract-json.js";
10
+ import { describeSchema, serializeSmsMatcher } from "./contract-serialization.js";
11
11
  import {
12
12
  PROVIDER_CONTRACT_SCHEMA_VERSION,
13
13
  type ProviderContractOperation,
14
14
  type ProviderContractSnapshot,
15
- } from "./contract-types";
15
+ } from "./contract-types.js";
16
16
  import type {
17
17
  HealthCheckSuite,
18
18
  HealthCheckUnsupported,
@@ -20,7 +20,7 @@ import type {
20
20
  OperationDefinition,
21
21
  OperationTransport,
22
22
  ProviderDefinition,
23
- } from "./types";
23
+ } from "./types.js";
24
24
 
25
25
  export {
26
26
  canonicalJson,
@@ -31,9 +31,7 @@ export {
31
31
  type ProviderContractSnapshot,
32
32
  };
33
33
 
34
- export function extractProviderContract(
35
- provider: ProviderDefinition,
36
- ): ProviderContractSnapshot {
34
+ export function extractProviderContract(provider: ProviderDefinition): ProviderContractSnapshot {
37
35
  const auth = extractAuth(provider.auth);
38
36
  const stealth = toJsonValue(provider.stealth);
39
37
  const proxy = toJsonValue(provider.proxy);
@@ -57,12 +55,8 @@ export function extractProviderContract(
57
55
  meta: toJsonValue(provider.meta) ?? null,
58
56
  operations: Object.entries(provider.operations)
59
57
  .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
60
- .map(([operationId, operation]) =>
61
- extractOperation(operationId, operation),
62
- ),
63
- ...(provider.allowedHosts
64
- ? { allowedHosts: [...provider.allowedHosts].sort() }
65
- : {}),
58
+ .map(([operationId, operation]) => extractOperation(operationId, operation)),
59
+ ...(provider.allowedHosts ? { allowedHosts: [...provider.allowedHosts].sort() } : {}),
66
60
  ...(stealth === undefined ? {} : { stealth }),
67
61
  ...(proxy === undefined ? {} : { proxy }),
68
62
  ...(stt === undefined ? {} : { stt }),
@@ -78,9 +72,7 @@ export function extractProviderContract(
78
72
  };
79
73
  }
80
74
 
81
- export function digestProviderContract(
82
- snapshot: ProviderContractSnapshot,
83
- ): string {
75
+ export function digestProviderContract(snapshot: ProviderContractSnapshot): string {
84
76
  return createHash("sha256").update(canonicalJson(snapshot)).digest("hex");
85
77
  }
86
78
 
@@ -105,9 +97,7 @@ function extractOperation(
105
97
  const upstream = toJsonValue(operation.upstream);
106
98
  const hints = toJsonValue(operation.hints);
107
99
  const healthCheck = extractHealthCheck(operation.healthCheck);
108
- const healthCheckUnsupported = extractHealthCheckUnsupported(
109
- operation.healthCheckUnsupported,
110
- );
100
+ const healthCheckUnsupported = extractHealthCheckUnsupported(operation.healthCheckUnsupported);
111
101
 
112
102
  return {
113
103
  id: operationId,
@@ -149,9 +139,7 @@ function extractAuth(value: ProviderDefinition["auth"]): JsonValue | undefined {
149
139
  });
150
140
  }
151
141
 
152
- function extractTransport(
153
- value: OperationTransport | undefined,
154
- ): JsonValue | undefined {
142
+ function extractTransport(value: OperationTransport | undefined): JsonValue | undefined {
155
143
  if (!value) return undefined;
156
144
  if (value.kind !== "sse") return toJsonValue(value);
157
145
  return compactObject({
@@ -164,9 +152,7 @@ function extractTransport(
164
152
  });
165
153
  }
166
154
 
167
- function extractHealthCheck(
168
- value: HealthCheckSuite | undefined,
169
- ): JsonValue | undefined {
155
+ function extractHealthCheck(value: HealthCheckSuite | undefined): JsonValue | undefined {
170
156
  if (!value) return undefined;
171
157
  return compactObject({
172
158
  interval: value.interval,
@@ -204,9 +190,7 @@ function extractHealthJourney(value: HealthJourneyDefinition): JsonValue {
204
190
  cooldown: value.cooldown,
205
191
  smsMatchers: toJsonValue(
206
192
  value.smsMatchers?.map((matcher) =>
207
- serializeSmsMatcher(
208
- copyRecordWithout(matcher, new Set(["extractOtp"])),
209
- ),
193
+ serializeSmsMatcher(copyRecordWithout(matcher, new Set(["extractOtp"]))),
210
194
  ),
211
195
  ),
212
196
  requiredSecrets: toJsonValue(value.requiredSecrets),