@apifuse/provider-sdk 2.2.0-beta.16 → 2.2.0-beta.17
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.
- package/AUTHORING.md +40 -0
- package/CHANGELOG.md +5 -0
- package/dist/config/loader.d.ts +19 -0
- package/dist/config/loader.js +59 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +1 -0
- package/dist/native-egress-policy.js +34 -21
- package/dist/native-ipv4.d.ts +22 -0
- package/dist/native-ipv4.js +98 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/native-network.d.ts +43 -7
- package/dist/runtime/native-network.js +567 -110
- package/dist/runtime/proxy-nodemaven.d.ts +7 -0
- package/dist/runtime/proxy-nodemaven.js +5 -5
- package/dist/server/serve.js +3 -1
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
- package/src/config/loader.ts +110 -18
- package/src/index.ts +5 -0
- package/src/native-egress-policy.ts +39 -33
- package/src/native-ipv4.ts +118 -0
- package/src/provider.ts +5 -0
- package/src/runtime/native-network.ts +734 -131
- package/src/runtime/proxy-nodemaven.ts +13 -5
- package/src/server/serve.ts +8 -1
- package/src/types.ts +6 -1
|
@@ -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 =
|
|
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 =
|
|
101
|
-
const password =
|
|
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);
|
package/dist/server/serve.js
CHANGED
|
@@ -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,10 @@ 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 and/or targetIpv4Cidrs. IPv4-literal grant targets match
|
|
1094
|
+
* only targetIpv4Cidrs, while DNS-name targets match only targetHostSuffixes.
|
|
1095
|
+
* CIDRs are exact IPv4 networks in a.b.c.d/nn form.
|
|
1092
1096
|
*
|
|
1093
1097
|
* Dynamic rules are ordered. The first rule whose source, target, port, and TLS
|
|
1094
1098
|
* selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
|
|
@@ -1101,7 +1105,8 @@ export interface NativeTcpDynamicEgressRule {
|
|
|
1101
1105
|
readonly sourceHostSuffixes?: readonly string[];
|
|
1102
1106
|
readonly sourcePorts?: readonly number[];
|
|
1103
1107
|
readonly sourcePortRanges?: readonly NativeTcpPortRange[];
|
|
1104
|
-
readonly targetHostSuffixes
|
|
1108
|
+
readonly targetHostSuffixes?: readonly string[];
|
|
1109
|
+
readonly targetIpv4Cidrs?: readonly string[];
|
|
1105
1110
|
readonly targetPorts?: readonly number[];
|
|
1106
1111
|
readonly targetPortRanges?: readonly NativeTcpPortRange[];
|
|
1107
1112
|
readonly tls: NativeTcpTlsMode;
|
package/package.json
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
1035
|
+
function resolveSmartproxyCountry(
|
|
1036
|
+
policy: ProviderProxyPolicy,
|
|
1037
|
+
ambientDefaults = true,
|
|
1038
|
+
): string | undefined {
|
|
989
1039
|
return (
|
|
990
|
-
policy.geo?.country ??
|
|
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 ??
|
|
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(
|
|
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(
|
|
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";
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
NativeTcpPortRange,
|
|
6
6
|
NativeTcpTlsMode,
|
|
7
7
|
} from "./types.js";
|
|
8
|
+
import { canonicalizeEgressHost, parseIpv4Cidr } from "./native-ipv4.js";
|
|
8
9
|
|
|
9
10
|
type NativeNetworkDeclaration = NonNullable<NativeProviderConfig["network"]>;
|
|
10
11
|
|
|
@@ -26,6 +27,7 @@ const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
|
|
|
26
27
|
sourcePorts: true,
|
|
27
28
|
sourcePortRanges: true,
|
|
28
29
|
targetHostSuffixes: true,
|
|
30
|
+
targetIpv4Cidrs: true,
|
|
29
31
|
targetPorts: true,
|
|
30
32
|
targetPortRanges: true,
|
|
31
33
|
tls: true,
|
|
@@ -55,6 +57,7 @@ export type DynamicEgressRuleSnapshot = {
|
|
|
55
57
|
readonly sourcePorts: readonly number[];
|
|
56
58
|
readonly sourcePortRanges: readonly NativeTcpPortRange[];
|
|
57
59
|
readonly targetHostSuffixes: readonly string[];
|
|
60
|
+
readonly targetIpv4Cidrs: readonly string[];
|
|
58
61
|
readonly targetPorts: readonly number[];
|
|
59
62
|
readonly targetPortRanges: readonly NativeTcpPortRange[];
|
|
60
63
|
readonly tls: NativeTcpTlsMode;
|
|
@@ -117,28 +120,12 @@ function dataArray(value: unknown, fieldPath: string): readonly unknown[] {
|
|
|
117
120
|
return result;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
function hasControlCharacter(value: string): boolean {
|
|
121
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
122
|
-
const code = value.charCodeAt(index);
|
|
123
|
-
if (code <= 31 || code === 127) return true;
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
123
|
function host(value: unknown, fieldPath: string, suffix = false): string {
|
|
129
|
-
if (
|
|
130
|
-
typeof value !== "string" ||
|
|
131
|
-
!value.trim() ||
|
|
132
|
-
hasControlCharacter(value) ||
|
|
133
|
-
/\s/.test(value) ||
|
|
134
|
-
value.includes("://")
|
|
135
|
-
)
|
|
136
|
-
fail(`${fieldPath} must be a non-empty hostname`);
|
|
137
|
-
if (value.includes("*"))
|
|
124
|
+
if (typeof value === "string" && value.includes("*"))
|
|
138
125
|
fail(`${fieldPath} must be an exact ${suffix ? "DNS suffix" : "hostname"}, not a wildcard`);
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
return
|
|
126
|
+
const canonical = canonicalizeEgressHost(value);
|
|
127
|
+
if (!canonical.ok) fail(`${fieldPath} must be a non-empty hostname`);
|
|
128
|
+
return canonical.host;
|
|
142
129
|
}
|
|
143
130
|
|
|
144
131
|
function port(value: unknown, fieldPath: string): number {
|
|
@@ -157,6 +144,24 @@ function hostSuffixes(value: unknown, fieldPath: string): readonly string[] {
|
|
|
157
144
|
);
|
|
158
145
|
}
|
|
159
146
|
|
|
147
|
+
function ipv4Cidrs(value: unknown, fieldPath: string): readonly string[] {
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
return dataArray(value, fieldPath).map((value, index) => {
|
|
150
|
+
const cidrPath = `${fieldPath}[${index}]`;
|
|
151
|
+
if (typeof value !== "string") fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
|
|
152
|
+
const parsed = parseIpv4Cidr(value);
|
|
153
|
+
if (!parsed.ok) {
|
|
154
|
+
if (parsed.reason === "non-canonical-network")
|
|
155
|
+
fail(`${cidrPath} must use the canonical network address with no host bits set`);
|
|
156
|
+
fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
|
|
157
|
+
}
|
|
158
|
+
const duplicateKey = `${parsed.network}/${parsed.prefix}`;
|
|
159
|
+
if (seen.has(duplicateKey)) fail(`${fieldPath} must not contain duplicate CIDRs`);
|
|
160
|
+
seen.add(duplicateKey);
|
|
161
|
+
return value;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
function ranges(value: unknown, fieldPath: string): readonly NativeTcpPortRange[] {
|
|
161
166
|
return dataArray(value, fieldPath).map((value, index) => {
|
|
162
167
|
const rangePath = `${fieldPath}[${index}]`;
|
|
@@ -212,9 +217,7 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
212
217
|
? []
|
|
213
218
|
: hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
|
|
214
219
|
if (sourceHost === undefined && sourceHostSuffixes.length === 0)
|
|
215
|
-
fail(
|
|
216
|
-
`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`,
|
|
217
|
-
);
|
|
220
|
+
fail(`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`);
|
|
218
221
|
const sourcePorts =
|
|
219
222
|
rule.sourcePorts === undefined
|
|
220
223
|
? []
|
|
@@ -224,15 +227,19 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
224
227
|
? []
|
|
225
228
|
: ranges(rule.sourcePortRanges, `${fieldPath}.sourcePortRanges`);
|
|
226
229
|
if (sourcePorts.length === 0 && sourcePortRanges.length === 0)
|
|
230
|
+
fail(`${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`);
|
|
231
|
+
const targetHostSuffixes =
|
|
232
|
+
rule.targetHostSuffixes === undefined
|
|
233
|
+
? []
|
|
234
|
+
: hostSuffixes(rule.targetHostSuffixes, `${fieldPath}.targetHostSuffixes`);
|
|
235
|
+
const targetIpv4Cidrs =
|
|
236
|
+
rule.targetIpv4Cidrs === undefined
|
|
237
|
+
? []
|
|
238
|
+
: ipv4Cidrs(rule.targetIpv4Cidrs, `${fieldPath}.targetIpv4Cidrs`);
|
|
239
|
+
if (targetHostSuffixes.length === 0 && targetIpv4Cidrs.length === 0)
|
|
227
240
|
fail(
|
|
228
|
-
`${fieldPath} must declare a non-empty
|
|
241
|
+
`${fieldPath} must declare a non-empty targetHostSuffixes or targetIpv4Cidrs list`,
|
|
229
242
|
);
|
|
230
|
-
const targetHostSuffixes = hostSuffixes(
|
|
231
|
-
rule.targetHostSuffixes,
|
|
232
|
-
`${fieldPath}.targetHostSuffixes`,
|
|
233
|
-
);
|
|
234
|
-
if (targetHostSuffixes.length === 0)
|
|
235
|
-
fail(`${fieldPath}.targetHostSuffixes must not be empty`);
|
|
236
243
|
const targetPorts =
|
|
237
244
|
rule.targetPorts === undefined
|
|
238
245
|
? []
|
|
@@ -242,15 +249,14 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
242
249
|
? []
|
|
243
250
|
: ranges(rule.targetPortRanges, `${fieldPath}.targetPortRanges`);
|
|
244
251
|
if (targetPorts.length === 0 && targetPortRanges.length === 0)
|
|
245
|
-
fail(
|
|
246
|
-
`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`,
|
|
247
|
-
);
|
|
252
|
+
fail(`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`);
|
|
248
253
|
return {
|
|
249
254
|
...(sourceHost === undefined ? {} : { sourceHost }),
|
|
250
255
|
sourceHostSuffixes,
|
|
251
256
|
sourcePorts,
|
|
252
257
|
sourcePortRanges,
|
|
253
258
|
targetHostSuffixes,
|
|
259
|
+
targetIpv4Cidrs,
|
|
254
260
|
targetPorts,
|
|
255
261
|
targetPortRanges,
|
|
256
262
|
tls: tls(rule.tls, `${fieldPath}.tls`),
|