@apifuse/provider-sdk 2.2.0-beta.16 → 2.2.0-beta.18

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.
@@ -3,6 +3,11 @@ export declare const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAM
3
3
  export declare const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
4
4
  export declare const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
5
5
  export declare const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
6
+ export type NodemavenCredentials = {
7
+ readonly username: string;
8
+ readonly password: string;
9
+ readonly filter?: string;
10
+ };
6
11
  /** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
7
12
  export type ProxyProtocol = "http" | "socks5";
8
13
  /**
@@ -23,6 +28,8 @@ export type NodemavenSessionWindow = {
23
28
  export declare function nodemavenSessionWindow(policy: ProviderProxyPolicy, now?: number): NodemavenSessionWindow;
24
29
  export type NodemavenSynthesisInput = {
25
30
  policy: ProviderProxyPolicy;
31
+ /** Explicit credentials; synthesis never reads process-global state. */
32
+ credentials: NodemavenCredentials;
26
33
  affinityKey: string | undefined;
27
34
  protocol: ProxyProtocol;
28
35
  poolIndex: number;
@@ -30,8 +30,8 @@ function readNodemavenUsername() {
30
30
  function readNodemavenPassword() {
31
31
  return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
32
32
  }
33
- function resolveNodemavenFilter() {
34
- const raw = process.env[NODEMAVEN_FILTER_ENV]?.trim().toLowerCase();
33
+ function resolveNodemavenFilter(value) {
34
+ const raw = value?.trim().toLowerCase();
35
35
  if (!raw)
36
36
  return DEFAULT_NODEMAVEN_FILTER;
37
37
  if (!NODEMAVEN_FILTERS.has(raw)) {
@@ -97,12 +97,12 @@ function selectPort(protocol, sid, poolIndex) {
97
97
  * There is no allocation API — geo/session are encoded in the username.
98
98
  */
99
99
  export function synthesizeNodemavenProxy(input) {
100
- const username = readNodemavenUsername();
101
- const password = readNodemavenPassword();
100
+ const username = input.credentials.username.trim();
101
+ const password = input.credentials.password.trim();
102
102
  if (!username || !password) {
103
103
  throw new Error(`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`);
104
104
  }
105
- const filter = resolveNodemavenFilter();
105
+ const filter = resolveNodemavenFilter(input.credentials.filter);
106
106
  const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
107
107
  const port = selectPort(input.protocol, sid, input.poolIndex);
108
108
  const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
@@ -16,7 +16,7 @@ import { createEnvContext } from "../runtime/env.js";
16
16
  import { executeOperation } from "../runtime/executor.js";
17
17
  import { createHttpClient } from "../runtime/http.js";
18
18
  import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
19
- import { createNativeNetworkClient } from "../runtime/native-network.js";
19
+ import { createEnvVendorCredentialResolver, createNativeNetworkClient, } from "../runtime/native-network.js";
20
20
  import { getProviderBaseUrl } from "../runtime/provider.js";
21
21
  import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
22
22
  import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
@@ -225,6 +225,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
225
225
  egress: provider.native.network,
226
226
  proxyPolicy: resolveNativeProxyPolicy(provider),
227
227
  affinityKey: proxyClientOptions.affinityKey,
228
+ credentials: createEnvVendorCredentialResolver(env),
228
229
  }),
229
230
  },
230
231
  }
@@ -312,6 +313,7 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
312
313
  egress: provider.native.network,
313
314
  proxyPolicy: resolveNativeProxyPolicy(provider),
314
315
  affinityKey: proxyClientOptions.affinityKey,
316
+ credentials: createEnvVendorCredentialResolver(createEnvContext(provider.secrets?.map((secret) => secret.name))),
315
317
  }),
316
318
  },
317
319
  }
package/dist/types.d.ts CHANGED
@@ -1089,6 +1089,11 @@ export interface NativeTcpEgressRule {
1089
1089
  /**
1090
1090
  * Bounded native TCP egress discovered through a declared bootstrap endpoint.
1091
1091
  * Host suffixes are exact DNS suffixes, not wildcard patterns.
1092
+ * Dynamic rules must declare at least one target host selector through
1093
+ * targetHostSuffixes, targetIpv4Cidrs, and/or targetIpv6Cidrs. Literal grant
1094
+ * sources and targets match only same-family selectors (except exact
1095
+ * sourceHost); DNS names match only host/suffix selectors. IPv4-mapped and
1096
+ * IPv4-compatible IPv6 literals are authorized only by IPv4 CIDRs.
1092
1097
  *
1093
1098
  * Dynamic rules are ordered. The first rule whose source, target, port, and TLS
1094
1099
  * selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
@@ -1099,9 +1104,13 @@ export interface NativeTcpEgressRule {
1099
1104
  export interface NativeTcpDynamicEgressRule {
1100
1105
  readonly sourceHost?: string;
1101
1106
  readonly sourceHostSuffixes?: readonly string[];
1107
+ readonly sourceIpv4Cidrs?: readonly string[];
1108
+ readonly sourceIpv6Cidrs?: readonly string[];
1102
1109
  readonly sourcePorts?: readonly number[];
1103
1110
  readonly sourcePortRanges?: readonly NativeTcpPortRange[];
1104
- readonly targetHostSuffixes: readonly string[];
1111
+ readonly targetHostSuffixes?: readonly string[];
1112
+ readonly targetIpv4Cidrs?: readonly string[];
1113
+ readonly targetIpv6Cidrs?: readonly string[];
1105
1114
  readonly targetPorts?: readonly number[];
1106
1115
  readonly targetPortRanges?: readonly NativeTcpPortRange[];
1107
1116
  readonly tls: NativeTcpTlsMode;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.16",
2
+ "version": "2.2.0-beta.18",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -7,7 +7,10 @@ import { Redis } from "ioredis";
7
7
  import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
8
8
  import {
9
9
  NODEMAVEN_DEFAULT_PROTOCOL,
10
+ NODEMAVEN_FILTER_ENV,
10
11
  NODEMAVEN_MAX_POOL_SIZE,
12
+ NODEMAVEN_PASSWORD_ENV,
13
+ NODEMAVEN_USERNAME_ENV,
11
14
  type ProxyProtocol,
12
15
  hasNodemavenCredentials,
13
16
  nodemavenPoolSize,
@@ -683,7 +686,7 @@ export async function resolveProxy(
683
686
  * for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
684
687
  * Override per call via ProxyResolutionOptions.protocol (harness/tests).
685
688
  */
686
- const VENDOR_DEFAULT_PROTOCOL: Record<ProxyVendorName, ProxyProtocol> = {
689
+ export const VENDOR_DEFAULT_PROTOCOL: Readonly<Record<ProxyVendorName, ProxyProtocol>> = {
687
690
  smartproxy: "http",
688
691
  nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
689
692
  };
@@ -708,21 +711,59 @@ export function assertTunnelingScheme(url: string): void {
708
711
  }
709
712
  }
710
713
 
711
- async function resolveWithVendor(
714
+ export type ProxyVendorResolutionContext = {
715
+ readonly protocol: ProxyProtocol;
716
+ readonly poolIndex: number;
717
+ readonly refreshEpoch: number;
718
+ /** Explicit vendor credentials. Omit only on the legacy ambient-env path. */
719
+ readonly credentials?: Readonly<Record<string, string>>;
720
+ /** Disable non-policy env defaults for deterministic injected adapters. */
721
+ readonly ambientDefaults?: boolean;
722
+ /** Disable env-discovered Redis sharing for deterministic injected adapters. */
723
+ readonly sharedCache?: boolean;
724
+ };
725
+
726
+ export async function resolveWithVendor(
712
727
  vendor: ProxyVendorName,
713
728
  policy: ProviderProxyPolicy,
714
729
  options: ProxyResolutionOptions,
715
- context: { protocol: ProxyProtocol; poolIndex: number; refreshEpoch: number },
730
+ context: ProxyVendorResolutionContext,
716
731
  ): Promise<ResolvedProxyConfig> {
717
732
  if (vendor === "nodemaven") {
718
733
  const startedAt = Date.now();
734
+ const username = (
735
+ context.credentials === undefined
736
+ ? process.env[NODEMAVEN_USERNAME_ENV]
737
+ : context.credentials[NODEMAVEN_USERNAME_ENV]
738
+ )?.trim();
739
+ const password = (
740
+ context.credentials === undefined
741
+ ? process.env[NODEMAVEN_PASSWORD_ENV]
742
+ : context.credentials[NODEMAVEN_PASSWORD_ENV]
743
+ )?.trim();
744
+ const filter =
745
+ context.credentials === undefined
746
+ ? process.env[NODEMAVEN_FILTER_ENV]
747
+ : context.credentials[NODEMAVEN_FILTER_ENV];
748
+ if (!username || !password) {
749
+ throw new ProxyResolutionError(
750
+ "PROXY_ALLOCATION_FAILED",
751
+ `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
752
+ { vendor: "nodemaven" },
753
+ );
754
+ }
719
755
  const synthesized = synthesizeNodemavenProxy({
720
756
  policy,
757
+ credentials: {
758
+ username,
759
+ password,
760
+ ...(filter ? { filter } : {}),
761
+ },
721
762
  affinityKey: options.affinityKey,
722
763
  protocol: context.protocol,
723
764
  poolIndex: context.poolIndex,
724
765
  refreshEpoch: context.refreshEpoch,
725
- country: resolveSmartproxyCountry(policy),
766
+ country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
726
767
  });
727
768
  options.telemetry?.recordProxyResolution({
728
769
  provider: "nodemaven",
@@ -746,7 +787,11 @@ async function resolveWithVendor(
746
787
  }
747
788
 
748
789
  // smartproxy allocation-style vendor.
749
- const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
790
+ const appKey = (
791
+ context.credentials === undefined
792
+ ? process.env[SMARTPROXY_APP_KEY_ENV]
793
+ : context.credentials[SMARTPROXY_APP_KEY_ENV]
794
+ )?.trim();
750
795
  if (!appKey) {
751
796
  // Guarded by vendorHasCredentials; treated as a vendor-internal failure.
752
797
  throw new ProxyResolutionError(
@@ -755,13 +800,15 @@ async function resolveWithVendor(
755
800
  { vendor: "smartproxy" },
756
801
  );
757
802
  }
758
- const lifetimeMinutes = resolveSmartproxyLifetime(policy);
803
+ const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
759
804
  const allocated = await allocateSmartproxy(
760
805
  policy,
761
806
  appKey,
762
807
  lifetimeMinutes,
763
808
  options.affinityKey,
764
809
  context.protocol,
810
+ context.ambientDefaults !== false,
811
+ context.sharedCache !== false,
765
812
  );
766
813
  options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
767
814
  const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
@@ -985,15 +1032,22 @@ export function mapFlatAttempt(
985
1032
  return { vendorIndex: 0, poolIndex: 0 };
986
1033
  }
987
1034
 
988
- function resolveSmartproxyCountry(policy: ProviderProxyPolicy): string | undefined {
1035
+ function resolveSmartproxyCountry(
1036
+ policy: ProviderProxyPolicy,
1037
+ ambientDefaults = true,
1038
+ ): string | undefined {
989
1039
  return (
990
- policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined
1040
+ policy.geo?.country ??
1041
+ (ambientDefaults
1042
+ ? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
1043
+ : undefined)
991
1044
  );
992
1045
  }
993
1046
 
994
- function resolveSmartproxyLifetime(policy: ProviderProxyPolicy): number {
1047
+ function resolveSmartproxyLifetime(policy: ProviderProxyPolicy, ambientDefaults = true): number {
995
1048
  const configuredLifetime =
996
- policy.session?.lifetimeMinutes ?? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
1049
+ policy.session?.lifetimeMinutes ??
1050
+ (ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
997
1051
  return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
998
1052
  }
999
1053
 
@@ -1024,15 +1078,21 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
1024
1078
 
1025
1079
  function buildSmartproxyCacheKey(
1026
1080
  policy: ProviderProxyPolicy,
1081
+ appKey: string,
1027
1082
  affinityKey: string | undefined,
1028
1083
  lifetimeMinutes: number,
1029
1084
  protocol: ProxyProtocol,
1085
+ ambientDefaults = true,
1030
1086
  ): string {
1031
1087
  const poolSize = resolveSmartproxyPoolSize(policy);
1032
1088
  return JSON.stringify({
1033
1089
  provider: "smartproxy",
1090
+ credentialHash: createHash("sha256")
1091
+ .update("apifuse-smartproxy-credential:v1\0")
1092
+ .update(appKey)
1093
+ .digest("hex"),
1034
1094
  protocol,
1035
- country: resolveSmartproxyCountry(policy),
1095
+ country: resolveSmartproxyCountry(policy, ambientDefaults),
1036
1096
  affinity: policy.session?.affinity ?? "request",
1037
1097
  affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
1038
1098
  lifetimeMinutes,
@@ -1046,8 +1106,17 @@ async function allocateSmartproxy(
1046
1106
  lifetimeMinutes: number,
1047
1107
  affinityKey: string | undefined,
1048
1108
  protocol: ProxyProtocol,
1109
+ ambientDefaults = true,
1110
+ sharedCache = true,
1049
1111
  ): Promise<SmartproxyAllocationResult> {
1050
- const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes, protocol);
1112
+ const cacheKey = buildSmartproxyCacheKey(
1113
+ policy,
1114
+ appKey,
1115
+ affinityKey,
1116
+ lifetimeMinutes,
1117
+ protocol,
1118
+ ambientDefaults,
1119
+ );
1051
1120
  const startedAt = Date.now();
1052
1121
  const now = startedAt;
1053
1122
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
@@ -1055,7 +1124,15 @@ async function allocateSmartproxy(
1055
1124
  const cached = proxyCache.get(cacheKey);
1056
1125
  if (!skipCached && cached && isFresh(cached, now)) {
1057
1126
  if (shouldSoftRefresh(cached, now)) {
1058
- void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol);
1127
+ void refreshSmartproxyPool(
1128
+ cacheKey,
1129
+ policy,
1130
+ appKey,
1131
+ lifetimeMinutes,
1132
+ protocol,
1133
+ ambientDefaults,
1134
+ sharedCache,
1135
+ );
1059
1136
  return {
1060
1137
  pool: cached,
1061
1138
  telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
@@ -1069,7 +1146,7 @@ async function allocateSmartproxy(
1069
1146
  };
1070
1147
  }
1071
1148
 
1072
- if (!skipCached) {
1149
+ if (!skipCached && sharedCache) {
1073
1150
  const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
1074
1151
  if (redisResult) return redisResult;
1075
1152
  }
@@ -1092,6 +1169,8 @@ async function allocateSmartproxy(
1092
1169
  lifetimeMinutes,
1093
1170
  startedAt,
1094
1171
  protocol,
1172
+ ambientDefaults,
1173
+ sharedCache,
1095
1174
  ).finally(() => {
1096
1175
  proxyInflight.delete(cacheKey);
1097
1176
  });
@@ -1131,6 +1210,8 @@ async function refreshSmartproxyPool(
1131
1210
  appKey: string,
1132
1211
  lifetimeMinutes: number,
1133
1212
  protocol: ProxyProtocol,
1213
+ ambientDefaults: boolean,
1214
+ sharedCache: boolean,
1134
1215
  ): Promise<void> {
1135
1216
  try {
1136
1217
  await allocateSmartproxyShared(
@@ -1140,6 +1221,8 @@ async function refreshSmartproxyPool(
1140
1221
  lifetimeMinutes,
1141
1222
  Date.now(),
1142
1223
  protocol,
1224
+ ambientDefaults,
1225
+ sharedCache,
1143
1226
  {
1144
1227
  background: true,
1145
1228
  },
@@ -1156,9 +1239,11 @@ async function allocateSmartproxyShared(
1156
1239
  lifetimeMinutes: number,
1157
1240
  startedAt: number,
1158
1241
  protocol: ProxyProtocol,
1242
+ ambientDefaults: boolean,
1243
+ sharedCache: boolean,
1159
1244
  options: { background?: boolean } = {},
1160
1245
  ): Promise<SmartproxyAllocationResult> {
1161
- const redis = getProxyRedis();
1246
+ const redis = sharedCache ? getProxyRedis() : undefined;
1162
1247
  if (!redis || !(await ensureRedisReady(redis))) {
1163
1248
  return await allocateAndStoreSmartproxyPool(
1164
1249
  cacheKey,
@@ -1166,7 +1251,7 @@ async function allocateSmartproxyShared(
1166
1251
  appKey,
1167
1252
  lifetimeMinutes,
1168
1253
  startedAt,
1169
- { cacheStatus: "allocator", protocol },
1254
+ { cacheStatus: "allocator", protocol, ambientDefaults },
1170
1255
  );
1171
1256
  }
1172
1257
 
@@ -1192,6 +1277,7 @@ async function allocateSmartproxyShared(
1192
1277
  redis,
1193
1278
  poolKey,
1194
1279
  protocol,
1280
+ ambientDefaults,
1195
1281
  },
1196
1282
  );
1197
1283
  } finally {
@@ -1348,6 +1434,7 @@ async function allocateAndStoreSmartproxyPool(
1348
1434
  redis?: ProxyRedisClient;
1349
1435
  poolKey?: string;
1350
1436
  protocol: ProxyProtocol;
1437
+ ambientDefaults: boolean;
1351
1438
  },
1352
1439
  ): Promise<SmartproxyAllocationResult> {
1353
1440
  const poolSize = resolveSmartproxyPoolSize(policy);
@@ -1357,6 +1444,7 @@ async function allocateAndStoreSmartproxyPool(
1357
1444
  lifetimeMinutes,
1358
1445
  poolSize,
1359
1446
  options.protocol,
1447
+ options.ambientDefaults,
1360
1448
  );
1361
1449
  const allocatorStartedAt = Date.now();
1362
1450
  const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
@@ -1427,7 +1515,7 @@ async function allocateAndStoreSmartproxyPool(
1427
1515
  expiresAt: allocatedAt + ttlMs,
1428
1516
  diagnostics: {
1429
1517
  provider: "smartproxy",
1430
- country: resolveSmartproxyCountry(policy) ?? "default",
1518
+ country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
1431
1519
  lifetimeMinutes,
1432
1520
  affinity: policy.session?.affinity ?? "request",
1433
1521
  rawConnect: true,
@@ -1590,6 +1678,7 @@ function buildSmartproxyAllocatorUrl(
1590
1678
  lifetimeMinutes: number,
1591
1679
  poolSize: number,
1592
1680
  protocol: ProxyProtocol,
1681
+ ambientDefaults = true,
1593
1682
  ): string {
1594
1683
  const params = new URLSearchParams({
1595
1684
  app_key: appKey,
@@ -1600,7 +1689,7 @@ function buildSmartproxyAllocatorUrl(
1600
1689
  format: "txt",
1601
1690
  lb: "\\n",
1602
1691
  });
1603
- const country = resolveSmartproxyCountry(policy);
1692
+ const country = resolveSmartproxyCountry(policy, ambientDefaults);
1604
1693
  if (country) {
1605
1694
  params.set("cc", country);
1606
1695
  }
@@ -1681,8 +1770,11 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
1681
1770
  }
1682
1771
 
1683
1772
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1773
+ const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
1774
+ if (!appKey) return undefined;
1684
1775
  const cacheKey = buildSmartproxyCacheKey(
1685
1776
  policy,
1777
+ appKey,
1686
1778
  options.affinityKey,
1687
1779
  lifetimeMinutes,
1688
1780
  options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
package/src/index.ts CHANGED
@@ -72,6 +72,7 @@ export { executeOperation } from "./runtime/executor.js";
72
72
  export { createHttpClient } from "./runtime/http.js";
73
73
  export {
74
74
  createNativeNetworkClient,
75
+ createEnvVendorCredentialResolver,
75
76
  deriveNativeCredentialAffinityKey,
76
77
  NativeEgressGrantExpiredError,
77
78
  NativeEgressNotDeclaredError,
@@ -81,10 +82,14 @@ export {
81
82
  resolveNativeGatewayProxy,
82
83
  type NativeGatewayProxy,
83
84
  type NativeGatewayProxyResolutionInput,
85
+ type NativeGatewayProxySkipReason,
84
86
  type NativeGatewayProxySynthesizer,
87
+ type NativeGatewayProxySynthesisResult,
85
88
  type NativeGatewayProxySynthesisInput,
86
89
  type NativeNetworkClientOptions,
87
90
  type NativeNetworkErrorCode,
91
+ type VendorCredentialLookup,
92
+ type VendorCredentialResolver,
88
93
  } from "./runtime/native-network.js";
89
94
  export type { Insight, InsightSeverity } from "./runtime/insights.js";
90
95
  export { generateInsights } from "./runtime/insights.js";