@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.9

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.
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Impit } from "impit";
3
- import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader.js";
3
+ import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, policyResolvesRegistryVendorChain, ProxyResolutionError, resolvePolicyProxyPoolSpan, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, vendorFromResolvedSource, } from "../config/loader.js";
4
4
  import { SDKError, TransportError } from "../errors.js";
5
5
  import { getStealthProfile } from "../stealth/profiles.js";
6
6
  import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
@@ -8,7 +8,6 @@ import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefa
8
8
  import { appendQueryParams } from "./request-options.js";
9
9
  const DEFAULT_PROFILE = "chrome-146";
10
10
  const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
11
- const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
12
11
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
13
12
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
14
13
  const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
@@ -463,7 +462,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
463
462
  }
464
463
  return client;
465
464
  }
466
- async function resolveRequestProxy(options, proxyAttempt) {
465
+ async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
467
466
  const resolvedProxy = await resolveProxyConfigAsync({
468
467
  proxy: options?.proxy ?? clientOptions.proxy,
469
468
  upstream: clientOptions.upstream,
@@ -474,6 +473,10 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
474
473
  proxyAttemptOffset: options?.proxyAttemptOffset,
475
474
  retryAttemptOffset: proxyAttempt,
476
475
  }),
476
+ // The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
477
+ // preserving the client TLS fingerprint end-to-end.
478
+ transportProtocols: ["http", "socks5"],
479
+ ...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
477
480
  telemetry: clientOptions.telemetry,
478
481
  });
479
482
  if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
@@ -484,6 +487,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
484
487
  url: resolvedProxy.url,
485
488
  poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
486
489
  proxyHash: proxyEndpointHash(resolvedProxy.url),
490
+ vendor: vendorFromResolvedSource(resolvedProxy.source),
487
491
  };
488
492
  }
489
493
  const session = {
@@ -506,12 +510,28 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
506
510
  const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
507
511
  const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
508
512
  const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
509
- const policyProxyAttemptCap = Math.max(1, Math.min(MAX_POLICY_PROXY_RETRY_ATTEMPTS, clientOptions.proxyPolicy?.session?.poolSize ??
513
+ // Span the whole vendor chain: successive attempts rotate one vendor's
514
+ // pool, then fail over to the next vendor via the flat attempt index.
515
+ const policyProxy = clientOptions.proxyPolicy ??
510
516
  (typeof clientOptions.upstream?.proxy === "object"
511
- ? clientOptions.upstream.proxy.session?.poolSize
512
- : undefined) ??
513
- DEFAULT_SMARTPROXY_POOL_SIZE));
514
- const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
517
+ ? clientOptions.upstream.proxy
518
+ : undefined);
519
+ // The pool span is already bounded by each vendor's max pool size
520
+ // (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
521
+ // the chain's true maximum — a large NodeMaven pool stays fully
522
+ // reachable rather than being truncated at an arbitrary ceiling.
523
+ const policyProxyAttemptCap = Math.max(1, policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE);
524
+ // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
525
+ // successive attempts resolve a *different* endpoint, so it is the only one
526
+ // that may widen the attempt cap to the pool span, de-duplicate endpoints,
527
+ // and drive allocator stale-pool refresh. A static custom/decodo policy
528
+ // resolves the same URL every attempt: widening/refreshing it would resend
529
+ // the request dozens of times (up to maxAttempts × refreshes) and bypass
530
+ // retry:false and unsafe-method controls. Static policies therefore follow
531
+ // the ordinary transport-retry budget instead.
532
+ const rotatesRegistryChain = usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
533
+ const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
534
+ const dedupeAllocatorEndpoints = rotatesRegistryChain;
515
535
  let lastError;
516
536
  for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
517
537
  let stalePoolError;
@@ -527,7 +547,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
527
547
  return;
528
548
  attemptRecorded = true;
529
549
  clientOptions.telemetry?.recordProxyAttempt?.({
530
- provider: "smartproxy",
550
+ provider: attemptProxy?.vendor ?? "smartproxy",
531
551
  attempt: attempt + 1,
532
552
  ...(attemptProxy?.poolIndex === undefined
533
553
  ? {}
@@ -541,11 +561,16 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
541
561
  };
542
562
  try {
543
563
  assertNoUnsupportedFingerprintOverrides(options);
544
- attemptProxy = await resolveRequestProxy(options, attempt);
564
+ attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
545
565
  proxy = attemptProxy.url;
546
- if (proxy && usesPolicyAllocator) {
566
+ if (proxy && dedupeAllocatorEndpoints) {
567
+ // An under-filled allocation repeats endpoints (via the modulo
568
+ // pool mapping) before the flat offset crosses into the next
569
+ // vendor. Skip an already-tried endpoint and advance the offset
570
+ // rather than breaking — breaking here would strand the request on
571
+ // the primary vendor and never reach the fallback leg.
547
572
  if (attemptedProxies.has(proxy)) {
548
- break;
573
+ continue;
549
574
  }
550
575
  attemptedProxies.add(proxy);
551
576
  }
@@ -605,7 +630,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
605
630
  const normalizedError = normalizeStealthTransportError(error);
606
631
  recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
607
632
  lastError = normalizedError;
608
- if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
633
+ if (proxy && rotatesRegistryChain && isProxyPoolRefreshableError(normalizedError)) {
609
634
  stalePoolError = normalizedError;
610
635
  if (shouldRunProxyAuthDiagnostic(normalizedError)) {
611
636
  stalePoolDiagnosticProxy = proxy;
@@ -615,10 +640,27 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
615
640
  }
616
641
  break;
617
642
  }
618
- if (attempt + 1 <
619
- (stealthRetryOptions
620
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
621
- : maxAttempts) &&
643
+ // Cap the number of transport retries. For a policy-allocator chain,
644
+ // every attempt resolves a *different* endpoint/vendor (poolIndex
645
+ // rotates across the concatenated vendor pool spans), so a transport
646
+ // failure is a signal to advance to the next endpoint — potentially
647
+ // crossing into the fallback vendor — not to retry the same endpoint.
648
+ // Truncating that rotation at the per-endpoint retry budget would
649
+ // strand the request on the primary vendor and never reach the
650
+ // fallback, since the crossover only happens once the flat attempt
651
+ // index exceeds the primary vendor's pool size (~10-20).
652
+ // resolvePolicyTransportAttemptCap widens to the full chain span only
653
+ // for implicit, safe-method allocator requests; explicit retry
654
+ // policies (their documented `attempts` ceiling), unsafe methods, and
655
+ // static/non-registry vendors keep the per-endpoint retry budget.
656
+ const transportRetryCap = resolvePolicyTransportAttemptCap({
657
+ policy: policyProxy,
658
+ usesPolicyAllocator,
659
+ retryAttempts: stealthRetryOptions?.attempts ?? 1,
660
+ explicitRetry: hasExplicitRetryPolicy,
661
+ method,
662
+ });
663
+ if (attempt + 1 < transportRetryCap &&
622
664
  shouldRetryProxyTransportAttempt({
623
665
  error: normalizedError,
624
666
  explicitRetry: hasExplicitRetryPolicy,
@@ -634,7 +676,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
634
676
  throw normalizedError;
635
677
  }
636
678
  }
637
- if (usesPolicyAllocator &&
679
+ if (rotatesRegistryChain &&
638
680
  stalePoolError &&
639
681
  refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES) {
640
682
  await invalidateProxyResolutionCacheAsync({
@@ -312,11 +312,22 @@ function toErrorResponse(error, requestId) {
312
312
  },
313
313
  };
314
314
  }
315
+ // A masked internal error MUST NOT be advertised as retryable: without an
316
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
317
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
318
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
319
+ // We still refuse to leak message/stack — only the error class name (or the
320
+ // primitive type for non-Error throwables) is surfaced for ops triage.
315
321
  return {
316
322
  error: {
317
323
  code: "internal_error",
318
324
  message: "Internal error",
319
325
  ...(requestId ? { requestId } : {}),
326
+ details: {
327
+ retryable: false,
328
+ category: "internal_error",
329
+ errorClass: error instanceof Error ? error.name : typeof error,
330
+ },
320
331
  },
321
332
  };
322
333
  }
@@ -1,15 +1,15 @@
1
1
  import { z } from "zod";
2
2
  export declare const ConnectionModeSchema: z.ZodEnum<{
3
- credentials: "credentials";
4
3
  none: "none";
4
+ credentials: "credentials";
5
5
  oauth2: "oauth2";
6
6
  "platform-managed": "platform-managed";
7
7
  }>;
8
8
  export declare const OperationConnectionSchema: z.ZodObject<{
9
9
  id: z.ZodString;
10
10
  mode: z.ZodEnum<{
11
- credentials: "credentials";
12
11
  none: "none";
12
+ credentials: "credentials";
13
13
  oauth2: "oauth2";
14
14
  "platform-managed": "platform-managed";
15
15
  }>;
@@ -25,8 +25,8 @@ export declare const OperationRequestSchema: z.ZodObject<{
25
25
  connection: z.ZodOptional<z.ZodObject<{
26
26
  id: z.ZodString;
27
27
  mode: z.ZodEnum<{
28
- credentials: "credentials";
29
28
  none: "none";
29
+ credentials: "credentials";
30
30
  oauth2: "oauth2";
31
31
  "platform-managed": "platform-managed";
32
32
  }>;
@@ -55,21 +55,21 @@ export declare const OperationSuccessResponseSchema: z.ZodObject<{
55
55
  stale: z.ZodBoolean;
56
56
  keys: z.ZodArray<z.ZodString>;
57
57
  source: z.ZodOptional<z.ZodEnum<{
58
- loader: "loader";
59
- memory: "memory";
60
58
  mixed: "mixed";
61
59
  redis: "redis";
60
+ memory: "memory";
61
+ loader: "loader";
62
62
  }>>;
63
63
  }, z.core.$strip>>;
64
64
  retry: z.ZodOptional<z.ZodObject<{
65
65
  attempts: z.ZodNumber;
66
66
  retries: z.ZodNumber;
67
67
  preset: z.ZodOptional<z.ZodEnum<{
68
- aggressive_read: "aggressive_read";
69
68
  off: "off";
70
- rate_limit_aware: "rate_limit_aware";
71
- safe_read: "safe_read";
72
69
  transport_transient: "transport_transient";
70
+ safe_read: "safe_read";
71
+ aggressive_read: "aggressive_read";
72
+ rate_limit_aware: "rate_limit_aware";
73
73
  }>>;
74
74
  transport: z.ZodEnum<{
75
75
  native: "native";
@@ -101,8 +101,8 @@ export declare const AuthFlowRequestSchema: z.ZodObject<{
101
101
  connection: z.ZodOptional<z.ZodObject<{
102
102
  id: z.ZodString;
103
103
  mode: z.ZodEnum<{
104
- credentials: "credentials";
105
104
  none: "none";
105
+ credentials: "credentials";
106
106
  oauth2: "oauth2";
107
107
  "platform-managed": "platform-managed";
108
108
  }>;
package/dist/types.d.ts CHANGED
@@ -650,7 +650,25 @@ export type ConnectionMode = AuthMode;
650
650
  export type ProviderReviewed = "first-party" | "community" | "staging";
651
651
  export type ProviderAccessVisibility = "public" | "early_access";
652
652
  export type ProviderProxyMode = "disabled" | "optional" | "required";
653
- export type ProviderProxyProvider = "smartproxy" | "decodo" | "custom";
653
+ /**
654
+ * Proxy egress vendors. These are FOUR DISTINCT services — do not conflate them
655
+ * (a common mistake because the names collide with a well-known rebrand):
656
+ *
657
+ * - `smartproxy` — **api.smartproxy.org**, a residential proxy with an IP
658
+ * *extraction/allocation* API (app_key → a pool of raw `ip:port` CONNECT
659
+ * endpoints). This is our own vendor. It is NOT the company formerly named
660
+ * "Smartproxy". Credentials: `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
661
+ * - `nodemaven` — **gate.nodemaven.com**, a *gateway* proxy with static
662
+ * credentials; geo/session encoded in the username, no allocation API.
663
+ * - `decodo` — **decodo.com**, the *gateway* proxy that was named "Smartproxy"
664
+ * (smartproxy.com) before its 2025 rebrand to Decodo. Sticky sessions via
665
+ * username params. A different company from `smartproxy` above.
666
+ * **@deprecated** — unused; no managed adapter. Use `smartproxy`/`nodemaven`,
667
+ * or the `APIFUSE__PROXY__URL` bring-your-own escape hatch.
668
+ * - `custom` — **@deprecated** bring-your-own static proxy URL marker. The
669
+ * `APIFUSE__PROXY__URL` env still works without declaring this value.
670
+ */
671
+ export type ProviderProxyProvider = "smartproxy" | "nodemaven" | "decodo" | "custom";
654
672
  export type ProviderProxySessionAffinity = "request" | "operation" | "auth-flow" | "connection";
655
673
  export interface ProviderProxyPolicy {
656
674
  /**
@@ -658,7 +676,18 @@ export interface ProviderProxyPolicy {
658
676
  * certificate verification, and vendor allocator endpoints are SDK-owned.
659
677
  */
660
678
  mode: ProviderProxyMode;
679
+ /**
680
+ * @deprecated Use `providers: [...]` to declare an ordered vendor fallback
681
+ * chain. A single-element `providers` list is equivalent to this field.
682
+ */
661
683
  provider?: ProviderProxyProvider;
684
+ /**
685
+ * Ordered proxy-vendor fallback chain. The SDK tries each vendor in order and
686
+ * fails over to the next when a vendor lacks credentials or its allocation /
687
+ * transport is exhausted. When omitted, `provider` (or the platform default)
688
+ * is used as a single-vendor chain.
689
+ */
690
+ providers?: ProviderProxyProvider[];
662
691
  geo?: {
663
692
  /** ISO 3166-1 alpha-2 country code, for example KR or US. */
664
693
  country?: Iso3166Alpha2CountryCode;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.7",
2
+ "version": "2.2.0-beta.9",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -75,10 +75,11 @@
75
75
  "scripts": {
76
76
  "lint": "biome lint .",
77
77
  "lint:fix": "biome lint --write",
78
+ "lint:deprecated": "bun run scripts/lint-deprecated-usage.ts",
78
79
  "format": "biome format --write",
79
80
  "type-check": "tsc --noEmit",
80
81
  "test": "bun test",
81
- "check": "bun run lint && bun run type-check && bun run build",
82
+ "check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run build",
82
83
  "pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
83
84
  "pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
84
85
  "pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
@@ -91,7 +92,7 @@
91
92
  "@biomejs/biome": "^2.5.0",
92
93
  "@types/bun": "latest",
93
94
  "@types/node": "^25.9.3",
94
- "typescript": "7.0.2"
95
+ "typescript": "6.0.3"
95
96
  },
96
97
  "dependencies": {
97
98
  "@clack/prompts": "^1.5.1",