@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.
- package/AUTHORING.md +40 -0
- package/CHANGELOG.md +9 -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-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +4 -0
- package/dist/native-egress-policy.js +86 -23
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/native-network.d.ts +46 -7
- package/dist/runtime/native-network.js +590 -114
- 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 +10 -1
- package/package.json +1 -1
- package/src/config/loader.ts +110 -18
- package/src/index.ts +5 -0
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +105 -32
- package/src/provider.ts +5 -0
- package/src/runtime/native-network.ts +770 -136
- package/src/runtime/proxy-nodemaven.ts +13 -5
- package/src/server/serve.ts +8 -1
- package/src/types.ts +10 -1
|
@@ -4,18 +4,35 @@ import { connect as connectTlsSocket, type TLSSocket } from "node:tls";
|
|
|
4
4
|
|
|
5
5
|
import { SocksClient } from "socks";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
assertTunnelingScheme,
|
|
9
|
+
ProxyResolutionError,
|
|
10
|
+
SMARTPROXY_APP_KEY_ENV,
|
|
11
|
+
type ProxyProtocol,
|
|
12
|
+
VENDOR_DEFAULT_PROTOCOL,
|
|
13
|
+
resolveWithVendor,
|
|
14
|
+
} from "../config/loader.js";
|
|
8
15
|
import { TransportError } from "../errors.js";
|
|
9
16
|
import {
|
|
17
|
+
type DynamicEgressRuleSnapshot,
|
|
10
18
|
NativeEgressPolicyValidationError,
|
|
11
19
|
parseNativeEgressPolicy,
|
|
12
|
-
type DynamicEgressRuleSnapshot,
|
|
13
20
|
type StaticEgressRuleSnapshot,
|
|
14
21
|
} from "../native-egress-policy.js";
|
|
22
|
+
import {
|
|
23
|
+
canonicalizeEgressHost,
|
|
24
|
+
classifyEgressHost,
|
|
25
|
+
type EgressHostCanonicalizationFailure,
|
|
26
|
+
embeddedIpv4FromIpv6,
|
|
27
|
+
ipv4InCidr,
|
|
28
|
+
ipv6InCidr,
|
|
29
|
+
parseIpv6,
|
|
30
|
+
parseStrictIpv4,
|
|
31
|
+
} from "../native-address.js";
|
|
15
32
|
import type {
|
|
16
33
|
NativeNetworkClient,
|
|
17
|
-
NativeNetworkConnection,
|
|
18
34
|
NativeNetworkConnectInput,
|
|
35
|
+
NativeNetworkConnection,
|
|
19
36
|
NativeNetworkDynamicGrantOptions,
|
|
20
37
|
NativeNetworkEgressGrant,
|
|
21
38
|
NativeProviderConfig,
|
|
@@ -26,12 +43,17 @@ import type {
|
|
|
26
43
|
NativeTcpTlsMode,
|
|
27
44
|
ProviderProxyPolicy,
|
|
28
45
|
ProviderProxyProvider,
|
|
46
|
+
EnvContext,
|
|
29
47
|
} from "../types.js";
|
|
48
|
+
import { createEnvContext } from "./env.js";
|
|
30
49
|
import {
|
|
31
|
-
|
|
50
|
+
NODEMAVEN_FILTER_ENV,
|
|
51
|
+
NODEMAVEN_PASSWORD_ENV,
|
|
52
|
+
NODEMAVEN_USERNAME_ENV,
|
|
32
53
|
nodemavenSessionWindow,
|
|
33
54
|
synthesizeNodemavenProxy,
|
|
34
55
|
} from "./proxy-nodemaven.js";
|
|
56
|
+
import { redactSensitiveError } from "./request-options.js";
|
|
35
57
|
|
|
36
58
|
export type NativeNetworkErrorCode =
|
|
37
59
|
| "native_connection_aborted"
|
|
@@ -51,12 +73,13 @@ export type NativeNetworkErrorCode =
|
|
|
51
73
|
| "native_proxy_invalid";
|
|
52
74
|
|
|
53
75
|
export class NativeNetworkError extends TransportError {
|
|
54
|
-
constructor(message: string, code: NativeNetworkErrorCode) {
|
|
76
|
+
constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
|
|
55
77
|
const isEgressPolicyFailure =
|
|
56
78
|
code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
|
|
57
79
|
super(message, {
|
|
58
80
|
code,
|
|
59
81
|
status: 0,
|
|
82
|
+
...(cause ? { cause } : {}),
|
|
60
83
|
...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
|
|
61
84
|
});
|
|
62
85
|
this.name = "NativeNetworkError";
|
|
@@ -67,6 +90,11 @@ export class NativeNetworkError extends TransportError {
|
|
|
67
90
|
}
|
|
68
91
|
}
|
|
69
92
|
|
|
93
|
+
function safeDiagnosticEgressHost(value: unknown): string {
|
|
94
|
+
const canonical = canonicalizeEgressHost(value);
|
|
95
|
+
return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
export class NativeProxyExpiredError extends NativeNetworkError {
|
|
71
99
|
constructor(readonly expiresAt: string) {
|
|
72
100
|
super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
|
|
@@ -76,15 +104,19 @@ export class NativeProxyExpiredError extends NativeNetworkError {
|
|
|
76
104
|
|
|
77
105
|
/** Raised before transport setup when a native destination is not authorized. */
|
|
78
106
|
export class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
107
|
+
readonly host: string;
|
|
108
|
+
|
|
79
109
|
constructor(
|
|
80
|
-
|
|
110
|
+
host: string,
|
|
81
111
|
readonly port: number,
|
|
82
112
|
readonly tls: "required" | "disabled",
|
|
83
113
|
) {
|
|
114
|
+
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
84
115
|
super(
|
|
85
|
-
`Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${
|
|
116
|
+
`Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
|
|
86
117
|
"native_egress_not_declared",
|
|
87
118
|
);
|
|
119
|
+
this.host = diagnosticHost;
|
|
88
120
|
this.name = "NativeEgressNotDeclaredError";
|
|
89
121
|
}
|
|
90
122
|
}
|
|
@@ -94,16 +126,20 @@ export class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
|
94
126
|
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
95
127
|
*/
|
|
96
128
|
export class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
129
|
+
readonly host: string;
|
|
130
|
+
|
|
97
131
|
constructor(
|
|
98
|
-
|
|
132
|
+
host: string,
|
|
99
133
|
readonly port: number,
|
|
100
134
|
readonly tls: "required" | "disabled",
|
|
101
135
|
readonly expiresAt: string,
|
|
102
136
|
) {
|
|
137
|
+
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
103
138
|
super(
|
|
104
|
-
`Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${
|
|
139
|
+
`Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
|
|
105
140
|
"native_egress_grant_expired",
|
|
106
141
|
);
|
|
142
|
+
this.host = diagnosticHost;
|
|
107
143
|
this.name = "NativeEgressGrantExpiredError";
|
|
108
144
|
}
|
|
109
145
|
}
|
|
@@ -111,10 +147,7 @@ export class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
|
111
147
|
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
112
148
|
export class NativeIdleTimeoutError extends NativeNetworkError {
|
|
113
149
|
constructor() {
|
|
114
|
-
super(
|
|
115
|
-
"Native network socket timed out while reading.",
|
|
116
|
-
"native_connection_idle_timeout",
|
|
117
|
-
);
|
|
150
|
+
super("Native network socket timed out while reading.", "native_connection_idle_timeout");
|
|
118
151
|
this.name = "NativeIdleTimeoutError";
|
|
119
152
|
}
|
|
120
153
|
}
|
|
@@ -128,17 +161,38 @@ export type NativeGatewayProxySynthesisInput = {
|
|
|
128
161
|
readonly policy: ProviderProxyPolicy;
|
|
129
162
|
readonly affinityKey?: string;
|
|
130
163
|
readonly now: number;
|
|
164
|
+
readonly protocol: ProxyProtocol;
|
|
165
|
+
readonly credentials: VendorCredentialResolver;
|
|
131
166
|
};
|
|
132
167
|
|
|
168
|
+
export type VendorCredentialLookup =
|
|
169
|
+
| { readonly kind: "present"; readonly values: Readonly<Record<string, string>> }
|
|
170
|
+
| { readonly kind: "absent"; readonly missing: readonly string[] };
|
|
171
|
+
|
|
172
|
+
export type VendorCredentialResolver = (vendor: ProviderProxyProvider) => VendorCredentialLookup;
|
|
173
|
+
|
|
174
|
+
export type NativeGatewayProxySkipReason =
|
|
175
|
+
| { readonly kind: "credentials_absent"; readonly missing: readonly string[] }
|
|
176
|
+
| { readonly kind: "protocol_unsupported"; readonly protocol: string }
|
|
177
|
+
| { readonly kind: "allocation_failed"; readonly cause: Error }
|
|
178
|
+
| { readonly kind: "credential_lookup_failed"; readonly cause: Error };
|
|
179
|
+
|
|
180
|
+
export type NativeGatewayProxySynthesisResult =
|
|
181
|
+
| NativeGatewayProxy
|
|
182
|
+
| { readonly kind: "skipped"; readonly reason: NativeGatewayProxySkipReason }
|
|
183
|
+
| undefined;
|
|
184
|
+
|
|
133
185
|
/** A vendor adapter in the ordered native gateway resolution chain. */
|
|
134
186
|
export type NativeGatewayProxySynthesizer = (
|
|
135
187
|
input: NativeGatewayProxySynthesisInput,
|
|
136
|
-
) =>
|
|
188
|
+
) => NativeGatewayProxySynthesisResult | Promise<NativeGatewayProxySynthesisResult>;
|
|
137
189
|
|
|
138
190
|
export type NativeGatewayProxyResolutionInput = {
|
|
139
191
|
readonly policy: ProviderProxyPolicy;
|
|
140
192
|
readonly affinityKey?: string;
|
|
141
193
|
readonly now?: number;
|
|
194
|
+
readonly protocol?: ProxyProtocol;
|
|
195
|
+
readonly credentials?: VendorCredentialResolver;
|
|
142
196
|
readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
|
|
143
197
|
};
|
|
144
198
|
|
|
@@ -147,6 +201,10 @@ export type NativeNetworkClientOptions = {
|
|
|
147
201
|
readonly affinityKey?: string;
|
|
148
202
|
/** Stable credential/account identity; hashed before vendor synthesis. */
|
|
149
203
|
readonly credentialIdentity?: string;
|
|
204
|
+
/** Vendor credential lookup; defaults to the process EnvContext. */
|
|
205
|
+
readonly credentials?: VendorCredentialResolver;
|
|
206
|
+
/** Explicit CONNECT/SOCKS5 override; vendors otherwise choose their default. */
|
|
207
|
+
readonly proxyProtocol?: ProxyProtocol;
|
|
150
208
|
/** Vendor adapters in priority order within each policy vendor slot. */
|
|
151
209
|
readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
|
|
152
210
|
/** Warning-level lifecycle diagnostic sink. */
|
|
@@ -160,19 +218,74 @@ export type NativeNetworkClientOptions = {
|
|
|
160
218
|
readonly grantTcpEgress?: (input: NativeNetworkDynamicGrantOptions) => NativeNetworkEgressGrant;
|
|
161
219
|
};
|
|
162
220
|
|
|
163
|
-
|
|
221
|
+
const VENDOR_CREDENTIAL_NAMES: Readonly<Partial<Record<ProviderProxyProvider, readonly string[]>>> =
|
|
222
|
+
{
|
|
223
|
+
smartproxy: [SMARTPROXY_APP_KEY_ENV],
|
|
224
|
+
nodemaven: [NODEMAVEN_USERNAME_ENV, NODEMAVEN_PASSWORD_ENV],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Build a resolver over the SDK's existing injectable environment context. */
|
|
228
|
+
export function createEnvVendorCredentialResolver(
|
|
229
|
+
env: EnvContext = createEnvContext(),
|
|
230
|
+
): VendorCredentialResolver {
|
|
231
|
+
return (vendor) => {
|
|
232
|
+
const names = VENDOR_CREDENTIAL_NAMES[vendor] ?? [];
|
|
233
|
+
const values: Record<string, string> = {};
|
|
234
|
+
const missing: string[] = [];
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
const value = env.get(name)?.trim();
|
|
237
|
+
if (value) values[name] = value;
|
|
238
|
+
else missing.push(name);
|
|
239
|
+
}
|
|
240
|
+
if (missing.length > 0 || names.length === 0) return { kind: "absent", missing };
|
|
241
|
+
if (vendor === "nodemaven") {
|
|
242
|
+
const filter = env.get(NODEMAVEN_FILTER_ENV)?.trim();
|
|
243
|
+
if (filter) values[NODEMAVEN_FILTER_ENV] = filter;
|
|
244
|
+
}
|
|
245
|
+
return { kind: "present", values };
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function skipped(reason: NativeGatewayProxySkipReason): NativeGatewayProxySynthesisResult {
|
|
250
|
+
return { kind: "skipped", reason };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function lookupCredentials(
|
|
164
254
|
input: NativeGatewayProxySynthesisInput,
|
|
165
|
-
):
|
|
166
|
-
|
|
255
|
+
): VendorCredentialLookup | { readonly kind: "error"; readonly cause: Error } {
|
|
256
|
+
try {
|
|
257
|
+
return input.credentials(input.vendor);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
kind: "error",
|
|
261
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
167
265
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
266
|
+
function synthesizeNodemavenGateway(
|
|
267
|
+
input: NativeGatewayProxySynthesisInput,
|
|
268
|
+
): NativeGatewayProxySynthesisResult {
|
|
269
|
+
if (input.vendor !== "nodemaven") return undefined;
|
|
270
|
+
const lookup = lookupCredentials(input);
|
|
271
|
+
if (lookup.kind === "error") {
|
|
272
|
+
return skipped({ kind: "credential_lookup_failed", cause: lookup.cause });
|
|
273
|
+
}
|
|
274
|
+
if (lookup.kind === "absent") {
|
|
275
|
+
return skipped({ kind: "credentials_absent", missing: lookup.missing });
|
|
276
|
+
}
|
|
171
277
|
const sessionWindow = nodemavenSessionWindow(input.policy, input.now);
|
|
172
278
|
const synthesized = synthesizeNodemavenProxy({
|
|
173
279
|
policy: input.policy,
|
|
280
|
+
credentials: {
|
|
281
|
+
username: lookup.values[NODEMAVEN_USERNAME_ENV] ?? "",
|
|
282
|
+
password: lookup.values[NODEMAVEN_PASSWORD_ENV] ?? "",
|
|
283
|
+
...(lookup.values[NODEMAVEN_FILTER_ENV]
|
|
284
|
+
? { filter: lookup.values[NODEMAVEN_FILTER_ENV] }
|
|
285
|
+
: {}),
|
|
286
|
+
},
|
|
174
287
|
affinityKey: input.affinityKey,
|
|
175
|
-
protocol:
|
|
288
|
+
protocol: input.protocol,
|
|
176
289
|
poolIndex: 0,
|
|
177
290
|
refreshEpoch: sessionWindow.refreshEpoch,
|
|
178
291
|
now: input.now,
|
|
@@ -187,7 +300,54 @@ function synthesizeNodemavenGateway(
|
|
|
187
300
|
};
|
|
188
301
|
}
|
|
189
302
|
|
|
303
|
+
async function synthesizeSmartproxyGateway(
|
|
304
|
+
input: NativeGatewayProxySynthesisInput,
|
|
305
|
+
): Promise<NativeGatewayProxySynthesisResult> {
|
|
306
|
+
if (input.vendor !== "smartproxy") return undefined;
|
|
307
|
+
const lookup = lookupCredentials(input);
|
|
308
|
+
if (lookup.kind === "error") {
|
|
309
|
+
return skipped({ kind: "credential_lookup_failed", cause: lookup.cause });
|
|
310
|
+
}
|
|
311
|
+
if (lookup.kind === "absent") {
|
|
312
|
+
return skipped({ kind: "credentials_absent", missing: lookup.missing });
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const resolved = await resolveWithVendor(
|
|
316
|
+
"smartproxy",
|
|
317
|
+
input.policy,
|
|
318
|
+
{
|
|
319
|
+
proxyPolicy: input.policy,
|
|
320
|
+
affinityKey: input.affinityKey,
|
|
321
|
+
protocol: input.protocol,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
protocol: input.protocol,
|
|
325
|
+
poolIndex: 0,
|
|
326
|
+
refreshEpoch: 0,
|
|
327
|
+
credentials: lookup.values,
|
|
328
|
+
ambientDefaults: false,
|
|
329
|
+
sharedCache: false,
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
if (!resolved.url) {
|
|
333
|
+
return skipped({ kind: "allocation_failed", cause: new Error("No endpoint returned") });
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
url: resolved.url,
|
|
337
|
+
vendor: "smartproxy",
|
|
338
|
+
sticky: isStickyPolicy(input.policy),
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
342
|
+
return skipped({
|
|
343
|
+
kind: "allocation_failed",
|
|
344
|
+
cause: redactSensitiveError(cause, Object.values(lookup.values)),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
190
349
|
const DEFAULT_GATEWAY_SYNTHESIZERS: readonly NativeGatewayProxySynthesizer[] = [
|
|
350
|
+
synthesizeSmartproxyGateway,
|
|
191
351
|
synthesizeNodemavenGateway,
|
|
192
352
|
];
|
|
193
353
|
|
|
@@ -216,35 +376,135 @@ function resolveNativeVendorChain(policy: ProviderProxyPolicy): ProviderProxyPro
|
|
|
216
376
|
return chain;
|
|
217
377
|
}
|
|
218
378
|
|
|
219
|
-
|
|
220
|
-
|
|
379
|
+
type NativeGatewayVendorSkip = {
|
|
380
|
+
readonly vendor: ProviderProxyProvider;
|
|
381
|
+
readonly reason: NativeGatewayProxySkipReason | { readonly kind: "adapter_unavailable" };
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
function isProxyProtocol(value: unknown): value is ProxyProtocol {
|
|
385
|
+
return value === "http" || value === "socks5";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isSkippedSynthesis(
|
|
389
|
+
result: Exclude<NativeGatewayProxySynthesisResult, undefined>,
|
|
390
|
+
): result is { readonly kind: "skipped"; readonly reason: NativeGatewayProxySkipReason } {
|
|
391
|
+
return "kind" in result && result.kind === "skipped";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sanitizeVendorResolutionCause(
|
|
395
|
+
error: unknown,
|
|
396
|
+
vendor: ProviderProxyProvider,
|
|
397
|
+
credentials: VendorCredentialResolver,
|
|
398
|
+
): Error {
|
|
399
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
400
|
+
try {
|
|
401
|
+
const lookup = credentials(vendor);
|
|
402
|
+
return lookup.kind === "present"
|
|
403
|
+
? redactSensitiveError(cause, Object.values(lookup.values))
|
|
404
|
+
: cause;
|
|
405
|
+
} catch {
|
|
406
|
+
return cause;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function sanitizeVendorSkipReason(
|
|
411
|
+
reason: NativeGatewayProxySkipReason,
|
|
412
|
+
vendor: ProviderProxyProvider,
|
|
413
|
+
credentials: VendorCredentialResolver,
|
|
414
|
+
): NativeGatewayProxySkipReason {
|
|
415
|
+
return reason.kind === "allocation_failed" || reason.kind === "credential_lookup_failed"
|
|
416
|
+
? {
|
|
417
|
+
...reason,
|
|
418
|
+
cause: sanitizeVendorResolutionCause(reason.cause, vendor, credentials),
|
|
419
|
+
}
|
|
420
|
+
: reason;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function defaultVendorProtocol(vendor: ProviderProxyProvider): ProxyProtocol {
|
|
424
|
+
return vendor === "smartproxy" || vendor === "nodemaven"
|
|
425
|
+
? VENDOR_DEFAULT_PROTOCOL[vendor]
|
|
426
|
+
: "http";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function resolveNativeGatewayProxyDetailed(
|
|
221
430
|
input: NativeGatewayProxyResolutionInput,
|
|
222
|
-
): NativeGatewayProxy
|
|
223
|
-
if (input.policy.mode === "disabled") return
|
|
431
|
+
): Promise<{ proxy?: NativeGatewayProxy; skips: readonly NativeGatewayVendorSkip[] }> {
|
|
432
|
+
if (input.policy.mode === "disabled") return { skips: [] };
|
|
224
433
|
const synthesizers = input.gatewaySynthesizers ?? DEFAULT_GATEWAY_SYNTHESIZERS;
|
|
434
|
+
const credentials = input.credentials ?? createEnvVendorCredentialResolver();
|
|
225
435
|
const now = input.now ?? Date.now();
|
|
436
|
+
const skips: NativeGatewayVendorSkip[] = [];
|
|
226
437
|
for (const vendor of resolveNativeVendorChain(input.policy)) {
|
|
438
|
+
const protocol = input.protocol ?? defaultVendorProtocol(vendor);
|
|
439
|
+
if (!isProxyProtocol(protocol)) {
|
|
440
|
+
skips.push({ vendor, reason: { kind: "protocol_unsupported", protocol: String(protocol) } });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
let vendorSkip: NativeGatewayVendorSkip["reason"] | undefined;
|
|
227
444
|
for (const synthesize of synthesizers) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
445
|
+
try {
|
|
446
|
+
const resolved = await synthesize({
|
|
447
|
+
vendor,
|
|
448
|
+
policy: input.policy,
|
|
449
|
+
affinityKey: input.affinityKey,
|
|
450
|
+
now,
|
|
451
|
+
protocol,
|
|
452
|
+
credentials,
|
|
453
|
+
});
|
|
454
|
+
if (!resolved) continue;
|
|
455
|
+
if (isSkippedSynthesis(resolved)) {
|
|
456
|
+
vendorSkip = sanitizeVendorSkipReason(resolved.reason, vendor, credentials);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (resolved.vendor === vendor) {
|
|
460
|
+
assertTunnelingScheme(resolved.url);
|
|
461
|
+
return { proxy: resolved, skips };
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
vendorSkip = {
|
|
465
|
+
kind: "allocation_failed",
|
|
466
|
+
cause: sanitizeVendorResolutionCause(error, vendor, credentials),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
235
469
|
}
|
|
470
|
+
skips.push({ vendor, reason: vendorSkip ?? { kind: "adapter_unavailable" } });
|
|
236
471
|
}
|
|
237
|
-
return
|
|
472
|
+
return { skips };
|
|
238
473
|
}
|
|
239
474
|
|
|
240
|
-
|
|
475
|
+
/** Resolve the first configured native gateway, including allocation vendors. */
|
|
476
|
+
export async function resolveNativeGatewayProxy(
|
|
477
|
+
input: NativeGatewayProxyResolutionInput,
|
|
478
|
+
): Promise<NativeGatewayProxy | undefined> {
|
|
479
|
+
return (await resolveNativeGatewayProxyDetailed(input)).proxy;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function formatVendorSkip(skip: NativeGatewayVendorSkip): string {
|
|
483
|
+
switch (skip.reason.kind) {
|
|
484
|
+
case "credentials_absent":
|
|
485
|
+
return `${skip.vendor}: credentials absent (missing ${skip.reason.missing.join(", ") || "unspecified variables"})`;
|
|
486
|
+
case "protocol_unsupported":
|
|
487
|
+
return `${skip.vendor}: protocol ${skip.reason.protocol} is unsupported`;
|
|
488
|
+
case "allocation_failed":
|
|
489
|
+
return `${skip.vendor}: allocation failed (${skip.reason.cause.message})`;
|
|
490
|
+
case "credential_lookup_failed":
|
|
491
|
+
return `${skip.vendor}: credential lookup failed (${skip.reason.cause.message})`;
|
|
492
|
+
case "adapter_unavailable":
|
|
493
|
+
return `${skip.vendor}: no native adapter is registered`;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function proxyRequiredError(
|
|
498
|
+
policy: ProviderProxyPolicy,
|
|
499
|
+
skips: readonly NativeGatewayVendorSkip[],
|
|
500
|
+
): ProxyResolutionError {
|
|
241
501
|
const chain = resolveNativeVendorChain(policy).filter(
|
|
242
502
|
(vendor): vendor is "smartproxy" | "nodemaven" =>
|
|
243
503
|
vendor === "smartproxy" || vendor === "nodemaven",
|
|
244
504
|
);
|
|
245
505
|
return new ProxyResolutionError(
|
|
246
506
|
"PROXY_REQUIRED",
|
|
247
|
-
`Native proxy egress is required but
|
|
507
|
+
`Native proxy egress is required but the vendor chain was exhausted: ${skips.map(formatVendorSkip).join("; ") || "no vendors declared"}.`,
|
|
248
508
|
{ vendorChain: chain },
|
|
249
509
|
);
|
|
250
510
|
}
|
|
@@ -268,8 +528,8 @@ function timeoutError(): NativeNetworkError {
|
|
|
268
528
|
return new NativeNetworkError("Native connection timed out", "native_connection_timeout");
|
|
269
529
|
}
|
|
270
530
|
|
|
271
|
-
function failedError(): NativeNetworkError {
|
|
272
|
-
return new NativeNetworkError("Native connection failed", "native_connection_failed");
|
|
531
|
+
function failedError(cause?: Error): NativeNetworkError {
|
|
532
|
+
return new NativeNetworkError("Native connection failed", "native_connection_failed", cause);
|
|
273
533
|
}
|
|
274
534
|
|
|
275
535
|
function assertCanStart(signal: AbortSignal | undefined, deadline: Deadline): void {
|
|
@@ -302,7 +562,7 @@ async function waitForSocketEvent(
|
|
|
302
562
|
} else resolve();
|
|
303
563
|
};
|
|
304
564
|
const onReady = () => finish();
|
|
305
|
-
const onError = () => finish(failedError());
|
|
565
|
+
const onError = (cause: Error) => finish(failedError(cause));
|
|
306
566
|
const onClose = () => finish(failedError());
|
|
307
567
|
const onAbort = () => finish(abortError());
|
|
308
568
|
|
|
@@ -357,11 +617,129 @@ function parseSocks5Proxy(proxyUrl: string): {
|
|
|
357
617
|
}
|
|
358
618
|
}
|
|
359
619
|
|
|
620
|
+
function parseHttpConnectProxy(proxyUrl: string): {
|
|
621
|
+
host: string;
|
|
622
|
+
port: number;
|
|
623
|
+
userId?: string;
|
|
624
|
+
password?: string;
|
|
625
|
+
} {
|
|
626
|
+
let parsed: URL;
|
|
627
|
+
try {
|
|
628
|
+
parsed = new URL(proxyUrl);
|
|
629
|
+
} catch {
|
|
630
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
631
|
+
}
|
|
632
|
+
const port = Number(parsed.port || "80");
|
|
633
|
+
if (
|
|
634
|
+
parsed.protocol !== "http:" ||
|
|
635
|
+
!parsed.hostname ||
|
|
636
|
+
!Number.isInteger(port) ||
|
|
637
|
+
port <= 0 ||
|
|
638
|
+
parsed.pathname !== "/" ||
|
|
639
|
+
parsed.search ||
|
|
640
|
+
parsed.hash
|
|
641
|
+
) {
|
|
642
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
return {
|
|
646
|
+
host: parsed.hostname,
|
|
647
|
+
port,
|
|
648
|
+
...(parsed.username ? { userId: decodeURIComponent(parsed.username) } : {}),
|
|
649
|
+
...(parsed.password ? { password: decodeURIComponent(parsed.password) } : {}),
|
|
650
|
+
};
|
|
651
|
+
} catch {
|
|
652
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const SOCKS5_REPLY_CODES = {
|
|
657
|
+
Failure: 0x01,
|
|
658
|
+
NotAllowed: 0x02,
|
|
659
|
+
NetworkUnreachable: 0x03,
|
|
660
|
+
HostUnreachable: 0x04,
|
|
661
|
+
ConnectionRefused: 0x05,
|
|
662
|
+
TTLExpired: 0x06,
|
|
663
|
+
CommandNotSupported: 0x07,
|
|
664
|
+
AddressNotSupported: 0x08,
|
|
665
|
+
} as const;
|
|
666
|
+
|
|
667
|
+
function socks5ReplyCode(error: Error): number | undefined {
|
|
668
|
+
const match = /Socks5 proxy rejected connection - ([A-Za-z]+)/i.exec(error.message);
|
|
669
|
+
if (!match?.[1]) return undefined;
|
|
670
|
+
const reply = Object.entries(SOCKS5_REPLY_CODES).find(
|
|
671
|
+
([name]) => name.toLowerCase() === match[1]?.toLowerCase(),
|
|
672
|
+
);
|
|
673
|
+
return reply?.[1];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sanitizeProxyFailureCause(
|
|
677
|
+
error: Error,
|
|
678
|
+
proxyUrl: string,
|
|
679
|
+
credentials: { readonly userId?: string; readonly password?: string },
|
|
680
|
+
): Error {
|
|
681
|
+
// socks' SocksClientError retains the live socket in options. It is neither
|
|
682
|
+
// useful diagnostic payload nor serializable, so preserve the original error
|
|
683
|
+
// while replacing only that options object with a socket-free snapshot.
|
|
684
|
+
const options = Reflect.get(error, "options");
|
|
685
|
+
if (options && typeof options === "object" && !Array.isArray(options)) {
|
|
686
|
+
const snapshot = { ...(options as Record<string, unknown>) };
|
|
687
|
+
delete snapshot.existing_socket;
|
|
688
|
+
try {
|
|
689
|
+
Reflect.set(error, "options", snapshot);
|
|
690
|
+
} catch {
|
|
691
|
+
// The recursive redactor below clones readonly diagnostics safely.
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const replyCode = socks5ReplyCode(error);
|
|
696
|
+
if (replyCode !== undefined) {
|
|
697
|
+
try {
|
|
698
|
+
Object.defineProperty(error, "socks5ReplyCode", {
|
|
699
|
+
value: replyCode,
|
|
700
|
+
configurable: true,
|
|
701
|
+
enumerable: true,
|
|
702
|
+
writable: false,
|
|
703
|
+
});
|
|
704
|
+
} catch {
|
|
705
|
+
// The reply label remains in message if an exotic error is immutable.
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let redactedProxyUrl = proxyUrl;
|
|
710
|
+
try {
|
|
711
|
+
const parsed = new URL(proxyUrl);
|
|
712
|
+
parsed.username = "[REDACTED]";
|
|
713
|
+
parsed.password = "[REDACTED]";
|
|
714
|
+
redactedProxyUrl = parsed.toString();
|
|
715
|
+
} catch {
|
|
716
|
+
// parseSocks5Proxy already validated this URL; keep a defensive fallback.
|
|
717
|
+
}
|
|
718
|
+
return redactSensitiveError(
|
|
719
|
+
error,
|
|
720
|
+
[
|
|
721
|
+
credentials.userId,
|
|
722
|
+
credentials.password,
|
|
723
|
+
credentials.userId !== undefined || credentials.password !== undefined
|
|
724
|
+
? `${credentials.userId ?? ""}:${credentials.password ?? ""}`
|
|
725
|
+
: undefined,
|
|
726
|
+
credentials.userId !== undefined || credentials.password !== undefined
|
|
727
|
+
? Buffer.from(`${credentials.userId ?? ""}:${credentials.password ?? ""}`).toString(
|
|
728
|
+
"base64",
|
|
729
|
+
)
|
|
730
|
+
: undefined,
|
|
731
|
+
].filter((value): value is string => typeof value === "string" && value.length > 0),
|
|
732
|
+
proxyUrl,
|
|
733
|
+
redactedProxyUrl,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
360
737
|
async function waitForSocksHandshake(
|
|
361
738
|
proxySocket: Socket,
|
|
362
739
|
promise: ReturnType<typeof SocksClient.createConnection>,
|
|
363
740
|
signal: AbortSignal | undefined,
|
|
364
741
|
deadline: Deadline,
|
|
742
|
+
sanitizeFailure: (error: Error) => Error,
|
|
365
743
|
): Promise<Socket> {
|
|
366
744
|
assertCanStart(signal, deadline);
|
|
367
745
|
return await new Promise<Socket>((resolve, reject) => {
|
|
@@ -391,7 +769,9 @@ async function waitForSocksHandshake(
|
|
|
391
769
|
finish(
|
|
392
770
|
error instanceof Error && /\b(?:timed out|timeout)\b/i.test(error.message)
|
|
393
771
|
? timeoutError()
|
|
394
|
-
: failedError(
|
|
772
|
+
: failedError(
|
|
773
|
+
sanitizeFailure(error instanceof Error ? error : new Error(String(error))),
|
|
774
|
+
),
|
|
395
775
|
),
|
|
396
776
|
);
|
|
397
777
|
});
|
|
@@ -423,7 +803,138 @@ async function connectSocksTunnel(
|
|
|
423
803
|
existing_socket: proxySocket,
|
|
424
804
|
...(remaining === undefined ? {} : { timeout: Math.max(1, remaining) }),
|
|
425
805
|
});
|
|
426
|
-
return await waitForSocksHandshake(proxySocket, handshake, input.signal, deadline)
|
|
806
|
+
return await waitForSocksHandshake(proxySocket, handshake, input.signal, deadline, (error) =>
|
|
807
|
+
sanitizeProxyFailureCause(error, proxy.url, parsed),
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function connectAuthority(host: string, port: number): string {
|
|
812
|
+
return `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function connectStatusError(statusLine: string, statusCode?: number): Error {
|
|
816
|
+
const error = new Error(
|
|
817
|
+
`HTTP CONNECT proxy rejected tunnel: ${statusLine || "invalid response"}`,
|
|
818
|
+
);
|
|
819
|
+
Object.defineProperties(error, {
|
|
820
|
+
connectStatusLine: {
|
|
821
|
+
value: statusLine || "invalid response",
|
|
822
|
+
configurable: true,
|
|
823
|
+
enumerable: true,
|
|
824
|
+
},
|
|
825
|
+
...(statusCode === undefined
|
|
826
|
+
? {}
|
|
827
|
+
: {
|
|
828
|
+
connectStatusCode: {
|
|
829
|
+
value: statusCode,
|
|
830
|
+
configurable: true,
|
|
831
|
+
enumerable: true,
|
|
832
|
+
},
|
|
833
|
+
}),
|
|
834
|
+
});
|
|
835
|
+
return error;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function waitForConnectResponse(
|
|
839
|
+
proxySocket: Socket,
|
|
840
|
+
signal: AbortSignal | undefined,
|
|
841
|
+
deadline: Deadline,
|
|
842
|
+
sanitizeFailure: (error: Error) => Error,
|
|
843
|
+
): Promise<Socket> {
|
|
844
|
+
assertCanStart(signal, deadline);
|
|
845
|
+
return await new Promise<Socket>((resolve, reject) => {
|
|
846
|
+
let settled = false;
|
|
847
|
+
let buffered = Buffer.alloc(0);
|
|
848
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
849
|
+
const cleanup = () => {
|
|
850
|
+
if (timer) clearTimeout(timer);
|
|
851
|
+
proxySocket.off("data", onData);
|
|
852
|
+
proxySocket.off("error", onError);
|
|
853
|
+
proxySocket.off("close", onClose);
|
|
854
|
+
signal?.removeEventListener("abort", onAbort);
|
|
855
|
+
};
|
|
856
|
+
const finish = (error?: Error) => {
|
|
857
|
+
if (settled) return;
|
|
858
|
+
settled = true;
|
|
859
|
+
cleanup();
|
|
860
|
+
if (error) {
|
|
861
|
+
proxySocket.on("error", () => undefined);
|
|
862
|
+
proxySocket.destroy();
|
|
863
|
+
reject(error);
|
|
864
|
+
} else resolve(proxySocket);
|
|
865
|
+
};
|
|
866
|
+
const onError = (cause: Error) => finish(failedError(sanitizeFailure(cause)));
|
|
867
|
+
const onClose = () =>
|
|
868
|
+
finish(failedError(sanitizeFailure(new Error("HTTP CONNECT proxy closed before response"))));
|
|
869
|
+
const onAbort = () => finish(abortError());
|
|
870
|
+
const onData = (chunk: Buffer) => {
|
|
871
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
872
|
+
if (buffered.length > 64 * 1024) {
|
|
873
|
+
finish(failedError(sanitizeFailure(connectStatusError("response headers too large"))));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const headerEnd = buffered.indexOf("\r\n\r\n");
|
|
877
|
+
if (headerEnd < 0) return;
|
|
878
|
+
const header = buffered.subarray(0, headerEnd).toString("latin1");
|
|
879
|
+
const statusLine = header.split("\r\n", 1)[0] ?? "";
|
|
880
|
+
const match = /^HTTP\/1\.[01] ([0-9]{3})(?: |$)/.exec(statusLine);
|
|
881
|
+
const statusCode = match?.[1] ? Number(match[1]) : undefined;
|
|
882
|
+
if (statusCode === undefined || statusCode < 200 || statusCode >= 300) {
|
|
883
|
+
finish(failedError(sanitizeFailure(connectStatusError(statusLine, statusCode))));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const remaining = buffered.subarray(headerEnd + 4);
|
|
887
|
+
cleanup();
|
|
888
|
+
if (remaining.length > 0) proxySocket.unshift(remaining);
|
|
889
|
+
finish();
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
proxySocket.on("data", onData);
|
|
893
|
+
proxySocket.once("error", onError);
|
|
894
|
+
proxySocket.once("close", onClose);
|
|
895
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
896
|
+
const remaining = remainingMs(deadline);
|
|
897
|
+
if (remaining !== undefined) timer = setTimeout(() => finish(timeoutError()), remaining);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function connectHttpTunnel(
|
|
902
|
+
proxy: NativeGatewayProxy,
|
|
903
|
+
input: NativeNetworkConnectInput,
|
|
904
|
+
deadline: Deadline,
|
|
905
|
+
beforeDestinationConnect: () => void,
|
|
906
|
+
): Promise<Socket> {
|
|
907
|
+
const parsed = parseHttpConnectProxy(proxy.url);
|
|
908
|
+
const proxySocket = await connectPlainSocket(parsed.host, parsed.port, input.signal, deadline);
|
|
909
|
+
const sanitizeFailure = (error: Error) => sanitizeProxyFailureCause(error, proxy.url, parsed);
|
|
910
|
+
try {
|
|
911
|
+
beforeDestinationConnect();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
proxySocket.destroy();
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
const authority = connectAuthority(input.host, input.port);
|
|
917
|
+
const authorization =
|
|
918
|
+
parsed.userId !== undefined || parsed.password !== undefined
|
|
919
|
+
? `Proxy-Authorization: Basic ${Buffer.from(`${parsed.userId ?? ""}:${parsed.password ?? ""}`).toString("base64")}\r\n`
|
|
920
|
+
: "";
|
|
921
|
+
const response = waitForConnectResponse(proxySocket, input.signal, deadline, sanitizeFailure);
|
|
922
|
+
proxySocket.write(
|
|
923
|
+
`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${authorization}Proxy-Connection: Keep-Alive\r\n\r\n`,
|
|
924
|
+
);
|
|
925
|
+
return await response;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function connectProxyTunnel(
|
|
929
|
+
proxy: NativeGatewayProxy,
|
|
930
|
+
input: NativeNetworkConnectInput,
|
|
931
|
+
deadline: Deadline,
|
|
932
|
+
beforeDestinationConnect: () => void,
|
|
933
|
+
): Promise<Socket> {
|
|
934
|
+
assertTunnelingScheme(proxy.url);
|
|
935
|
+
return new URL(proxy.url).protocol === "http:"
|
|
936
|
+
? await connectHttpTunnel(proxy, input, deadline, beforeDestinationConnect)
|
|
937
|
+
: await connectSocksTunnel(proxy, input, deadline, beforeDestinationConnect);
|
|
427
938
|
}
|
|
428
939
|
|
|
429
940
|
async function upgradeTls(
|
|
@@ -473,12 +984,15 @@ export function createNativeNetworkConnection(
|
|
|
473
984
|
const resetIdleTimer = () => {
|
|
474
985
|
clearIdleTimer();
|
|
475
986
|
if (idleTimeoutMs === undefined || socket.readableEnded || socket.destroyed) return;
|
|
476
|
-
idleTimer = setTimeout(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
987
|
+
idleTimer = setTimeout(
|
|
988
|
+
() => {
|
|
989
|
+
idleTimer = undefined;
|
|
990
|
+
if (socket.readableEnded || socket.destroyed) return;
|
|
991
|
+
closeReason = new NativeIdleTimeoutError();
|
|
992
|
+
socket.destroy(closeReason);
|
|
993
|
+
},
|
|
994
|
+
Math.max(0, idleTimeoutMs),
|
|
995
|
+
);
|
|
482
996
|
idleTimer.unref?.();
|
|
483
997
|
};
|
|
484
998
|
const clearLifecycle = () => {
|
|
@@ -541,7 +1055,7 @@ export function createNativeNetworkConnection(
|
|
|
541
1055
|
|
|
542
1056
|
const read = async (): Promise<Uint8Array | null> => {
|
|
543
1057
|
if (closeReason) throw closeReason;
|
|
544
|
-
if (terminalError) throw failedError();
|
|
1058
|
+
if (terminalError) throw failedError(terminalError);
|
|
545
1059
|
const chunk = socket.read() as Buffer | null;
|
|
546
1060
|
if (chunk) {
|
|
547
1061
|
resetIdleTimer();
|
|
@@ -596,7 +1110,7 @@ export function createNativeNetworkConnection(
|
|
|
596
1110
|
}
|
|
597
1111
|
await new Promise<void>((resolve, reject) => {
|
|
598
1112
|
socket.write(data, (error) => {
|
|
599
|
-
if (error) reject(failedError());
|
|
1113
|
+
if (error) reject(failedError(error));
|
|
600
1114
|
else resolve();
|
|
601
1115
|
});
|
|
602
1116
|
});
|
|
@@ -617,6 +1131,7 @@ export function createNativeNetworkConnection(
|
|
|
617
1131
|
async function resolveConnectionProxy(
|
|
618
1132
|
options: NativeNetworkClientOptions,
|
|
619
1133
|
input: NativeNetworkConnectInput,
|
|
1134
|
+
deadline: Deadline,
|
|
620
1135
|
): Promise<NativeGatewayProxy | undefined> {
|
|
621
1136
|
const policy = options.proxyPolicy;
|
|
622
1137
|
if (!policy || policy.mode === "disabled") return undefined;
|
|
@@ -626,13 +1141,54 @@ async function resolveConnectionProxy(
|
|
|
626
1141
|
(isStickyPolicy(policy) && options.credentialIdentity !== undefined
|
|
627
1142
|
? deriveNativeCredentialAffinityKey(options.credentialIdentity)
|
|
628
1143
|
: undefined);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1144
|
+
const resolution = await waitForProxyResolution(
|
|
1145
|
+
resolveNativeGatewayProxyDetailed({
|
|
1146
|
+
policy,
|
|
1147
|
+
affinityKey,
|
|
1148
|
+
protocol: options.proxyProtocol,
|
|
1149
|
+
credentials: options.credentials,
|
|
1150
|
+
gatewaySynthesizers: options.gatewaySynthesizers,
|
|
1151
|
+
}),
|
|
1152
|
+
input.signal,
|
|
1153
|
+
deadline,
|
|
1154
|
+
);
|
|
1155
|
+
if (!resolution.proxy && policy.mode === "required") {
|
|
1156
|
+
throw proxyRequiredError(policy, resolution.skips);
|
|
1157
|
+
}
|
|
1158
|
+
return resolution.proxy;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async function waitForProxyResolution<T>(
|
|
1162
|
+
promise: Promise<T>,
|
|
1163
|
+
signal: AbortSignal | undefined,
|
|
1164
|
+
deadline: Deadline,
|
|
1165
|
+
): Promise<T> {
|
|
1166
|
+
assertCanStart(signal, deadline);
|
|
1167
|
+
return await new Promise<T>((resolve, reject) => {
|
|
1168
|
+
let settled = false;
|
|
1169
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1170
|
+
const cleanup = () => {
|
|
1171
|
+
if (timer) clearTimeout(timer);
|
|
1172
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1173
|
+
};
|
|
1174
|
+
const finish = (value?: T, error?: unknown) => {
|
|
1175
|
+
if (settled) return;
|
|
1176
|
+
settled = true;
|
|
1177
|
+
cleanup();
|
|
1178
|
+
if (error !== undefined) reject(error);
|
|
1179
|
+
else resolve(value as T);
|
|
1180
|
+
};
|
|
1181
|
+
const onAbort = () => finish(undefined, abortError());
|
|
1182
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1183
|
+
const remaining = remainingMs(deadline);
|
|
1184
|
+
if (remaining !== undefined) {
|
|
1185
|
+
timer = setTimeout(() => finish(undefined, timeoutError()), remaining);
|
|
1186
|
+
}
|
|
1187
|
+
void promise.then(
|
|
1188
|
+
(value) => finish(value),
|
|
1189
|
+
(error) => finish(undefined, error),
|
|
1190
|
+
);
|
|
633
1191
|
});
|
|
634
|
-
if (!resolved && policy.mode === "required") throw proxyRequiredError(policy);
|
|
635
|
-
return resolved;
|
|
636
1192
|
}
|
|
637
1193
|
|
|
638
1194
|
type NativeConnectTls = "required" | "disabled";
|
|
@@ -648,10 +1204,6 @@ type StoredEgressGrant = {
|
|
|
648
1204
|
|
|
649
1205
|
export const NATIVE_EGRESS_EXPIRED_EVIDENCE_LIMIT = 256;
|
|
650
1206
|
|
|
651
|
-
function normalizeEgressHost(host: string): string {
|
|
652
|
-
return host.trim().toLowerCase().replace(/\.$/, "");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
1207
|
function invalidPolicy(message: string): NativeNetworkError {
|
|
656
1208
|
return new NativeNetworkError(message, "native_egress_policy_invalid");
|
|
657
1209
|
}
|
|
@@ -660,15 +1212,48 @@ function matchesDnsSuffix(host: string, suffix: string): boolean {
|
|
|
660
1212
|
return host === suffix || host.endsWith(`.${suffix}`);
|
|
661
1213
|
}
|
|
662
1214
|
|
|
663
|
-
|
|
664
|
-
|
|
1215
|
+
/** Internal validator-independent source selector matcher. */
|
|
1216
|
+
export function matchesSourceHost(rule: DynamicEgressRuleSnapshot, host: string): boolean {
|
|
1217
|
+
const kind = classifyEgressHost(host);
|
|
1218
|
+
if (kind === "numeric-ambiguous") return false;
|
|
1219
|
+
const hasSelector =
|
|
1220
|
+
rule.sourceHost !== undefined ||
|
|
1221
|
+
rule.sourceHostSuffixes.length > 0 ||
|
|
1222
|
+
rule.sourceIpv4Cidrs.length > 0 ||
|
|
1223
|
+
rule.sourceIpv6Cidrs.length > 0;
|
|
665
1224
|
if (!hasSelector) return false;
|
|
666
|
-
return
|
|
667
|
-
|
|
668
|
-
|
|
1225
|
+
if (host === rule.sourceHost) return true;
|
|
1226
|
+
return matchesHostFamilySelectors(
|
|
1227
|
+
host,
|
|
1228
|
+
rule.sourceHostSuffixes,
|
|
1229
|
+
rule.sourceIpv4Cidrs,
|
|
1230
|
+
rule.sourceIpv6Cidrs,
|
|
669
1231
|
);
|
|
670
1232
|
}
|
|
671
1233
|
|
|
1234
|
+
function matchesHostFamilySelectors(
|
|
1235
|
+
host: string,
|
|
1236
|
+
hostSuffixes: readonly string[],
|
|
1237
|
+
ipv4Cidrs: readonly string[],
|
|
1238
|
+
ipv6Cidrs: readonly string[],
|
|
1239
|
+
): boolean {
|
|
1240
|
+
const kind = classifyEgressHost(host);
|
|
1241
|
+
if (kind === "ipv4") {
|
|
1242
|
+
const address = parseStrictIpv4(host);
|
|
1243
|
+
return address !== undefined && ipv4Cidrs.some((cidr) => ipv4InCidr(address, cidr));
|
|
1244
|
+
}
|
|
1245
|
+
if (kind === "ipv4-mapped-ipv6") {
|
|
1246
|
+
const address = parseIpv6(host);
|
|
1247
|
+
const embedded = address && embeddedIpv4FromIpv6(address);
|
|
1248
|
+
return embedded !== undefined && ipv4Cidrs.some((cidr) => ipv4InCidr(embedded, cidr));
|
|
1249
|
+
}
|
|
1250
|
+
if (kind === "ipv6") {
|
|
1251
|
+
const address = parseIpv6(host);
|
|
1252
|
+
return address !== undefined && ipv6Cidrs.some((cidr) => ipv6InCidr(address, cidr));
|
|
1253
|
+
}
|
|
1254
|
+
return kind === "dns" && hostSuffixes.some((suffix) => matchesDnsSuffix(host, suffix));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
672
1257
|
function matchesPortSelectors(
|
|
673
1258
|
port: number,
|
|
674
1259
|
ports: readonly number[],
|
|
@@ -694,43 +1279,40 @@ function matchesDynamicRuleSelectors(
|
|
|
694
1279
|
rule: DynamicEgressRuleSnapshot,
|
|
695
1280
|
input: NativeNetworkDynamicGrantOptions,
|
|
696
1281
|
): boolean {
|
|
697
|
-
const sourceHost = normalizeEgressHost(input.sourceHost);
|
|
698
|
-
const targetHost = normalizeEgressHost(input.host);
|
|
699
1282
|
return (
|
|
700
|
-
matchesSourceHost(rule, sourceHost) &&
|
|
1283
|
+
matchesSourceHost(rule, input.sourceHost) &&
|
|
701
1284
|
matchesPortSelectors(input.sourcePort, rule.sourcePorts, rule.sourcePortRanges) &&
|
|
702
|
-
rule
|
|
1285
|
+
matchesDynamicTargetHost(rule, input.host) &&
|
|
703
1286
|
matchesPortSelectors(input.port, rule.targetPorts, rule.targetPortRanges) &&
|
|
704
1287
|
grantTlsFitsRule(input.tls, rule.tls)
|
|
705
1288
|
);
|
|
706
1289
|
}
|
|
707
1290
|
|
|
1291
|
+
function matchesDynamicTargetHost(rule: DynamicEgressRuleSnapshot, targetHost: string): boolean {
|
|
1292
|
+
return matchesHostFamilySelectors(
|
|
1293
|
+
targetHost,
|
|
1294
|
+
rule.targetHostSuffixes,
|
|
1295
|
+
rule.targetIpv4Cidrs,
|
|
1296
|
+
rule.targetIpv6Cidrs,
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
708
1300
|
function invalidGrant(message: string): NativeNetworkError {
|
|
709
1301
|
return new NativeNetworkError(message, "native_egress_grant_invalid");
|
|
710
1302
|
}
|
|
711
1303
|
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1304
|
+
function canonicalGrantHost(value: unknown): string {
|
|
1305
|
+
if (typeof value === "string" && value.includes("*"))
|
|
1306
|
+
throw invalidGrant("Native TCP egress grant hosts must be exact non-empty hostnames");
|
|
1307
|
+
const canonical = canonicalizeEgressHost(value);
|
|
1308
|
+
if (!canonical.ok)
|
|
1309
|
+
throw invalidGrant("Native TCP egress grant hosts must be exact non-empty hostnames");
|
|
1310
|
+
return canonical.host;
|
|
718
1311
|
}
|
|
719
1312
|
|
|
720
1313
|
function assertValidGrantInput(input: NativeNetworkDynamicGrantOptions): void {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
!normalizeEgressHost(input.host) ||
|
|
724
|
-
hasControlCharacter(input.sourceHost) ||
|
|
725
|
-
hasControlCharacter(input.host) ||
|
|
726
|
-
/\s/.test(input.sourceHost) ||
|
|
727
|
-
/\s/.test(input.host) ||
|
|
728
|
-
input.sourceHost.includes("://") ||
|
|
729
|
-
input.host.includes("://") ||
|
|
730
|
-
input.sourceHost.includes("*") ||
|
|
731
|
-
input.host.includes("*")
|
|
732
|
-
)
|
|
733
|
-
throw invalidGrant("Native TCP egress grant hosts must be exact non-empty hostnames");
|
|
1314
|
+
canonicalGrantHost(input.sourceHost);
|
|
1315
|
+
canonicalGrantHost(input.host);
|
|
734
1316
|
if (
|
|
735
1317
|
!Number.isSafeInteger(input.sourcePort) ||
|
|
736
1318
|
input.sourcePort < 1 ||
|
|
@@ -746,51 +1328,65 @@ function assertValidGrantInput(input: NativeNetworkDynamicGrantOptions): void {
|
|
|
746
1328
|
throw invalidGrant("Native TCP egress grant ttlMs must be a positive integer");
|
|
747
1329
|
}
|
|
748
1330
|
|
|
749
|
-
|
|
750
|
-
|
|
1331
|
+
type NativeConnectInputRejectionReason =
|
|
1332
|
+
| EgressHostCanonicalizationFailure
|
|
1333
|
+
| "port-range"
|
|
1334
|
+
| "inspection-failure";
|
|
1335
|
+
|
|
1336
|
+
function invalidNativeConnectInput(
|
|
1337
|
+
field: keyof NativeNetworkConnectInput,
|
|
1338
|
+
reason: NativeConnectInputRejectionReason,
|
|
1339
|
+
): NativeNetworkError {
|
|
1340
|
+
return new NativeNetworkError(
|
|
1341
|
+
`Native connection input rejected: field=${field}; reason=${reason}`,
|
|
1342
|
+
"native_egress_input_invalid",
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function inspectNativeConnectInputField<K extends keyof NativeNetworkConnectInput>(
|
|
751
1347
|
input: NativeNetworkConnectInput,
|
|
752
|
-
|
|
1348
|
+
field: K,
|
|
1349
|
+
): NativeNetworkConnectInput[K] {
|
|
753
1350
|
try {
|
|
754
|
-
|
|
755
|
-
const port = input.port;
|
|
756
|
-
const serverName = input.serverName;
|
|
757
|
-
const rejectUnauthorized = input.rejectUnauthorized;
|
|
758
|
-
const idleTimeoutMs = input.idleTimeoutMs;
|
|
759
|
-
const timeoutMs = input.timeoutMs;
|
|
760
|
-
const signal = input.signal;
|
|
761
|
-
const affinityKey = input.affinityKey;
|
|
762
|
-
const snapshot: NativeNetworkConnectInput = {
|
|
763
|
-
host,
|
|
764
|
-
port,
|
|
765
|
-
...(serverName === undefined ? {} : { serverName }),
|
|
766
|
-
...(rejectUnauthorized === undefined ? {} : { rejectUnauthorized }),
|
|
767
|
-
...(idleTimeoutMs === undefined ? {} : { idleTimeoutMs }),
|
|
768
|
-
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
769
|
-
...(signal === undefined ? {} : { signal }),
|
|
770
|
-
...(affinityKey === undefined ? {} : { affinityKey }),
|
|
771
|
-
};
|
|
772
|
-
if (
|
|
773
|
-
typeof snapshot.host !== "string" ||
|
|
774
|
-
!snapshot.host.trim() ||
|
|
775
|
-
hasControlCharacter(snapshot.host) ||
|
|
776
|
-
!Number.isInteger(snapshot.port) ||
|
|
777
|
-
snapshot.port < 1 ||
|
|
778
|
-
snapshot.port > 65_535
|
|
779
|
-
)
|
|
780
|
-
throw new TypeError("invalid native connection target");
|
|
781
|
-
return snapshot;
|
|
1351
|
+
return input[field];
|
|
782
1352
|
} catch {
|
|
783
|
-
throw
|
|
784
|
-
"Native connection input could not be inspected safely",
|
|
785
|
-
"native_egress_input_invalid",
|
|
786
|
-
);
|
|
1353
|
+
throw invalidNativeConnectInput(field, "inspection-failure");
|
|
787
1354
|
}
|
|
788
1355
|
}
|
|
789
1356
|
|
|
1357
|
+
/** Internal canonical snapshot shared by production and SDK transport test doubles. */
|
|
1358
|
+
export function snapshotNativeConnectInput(
|
|
1359
|
+
input: NativeNetworkConnectInput,
|
|
1360
|
+
): NativeNetworkConnectInput {
|
|
1361
|
+
const host = inspectNativeConnectInputField(input, "host");
|
|
1362
|
+
const canonicalHost = canonicalizeEgressHost(host);
|
|
1363
|
+
if (!canonicalHost.ok) throw invalidNativeConnectInput("host", canonicalHost.reason);
|
|
1364
|
+
const port = inspectNativeConnectInputField(input, "port");
|
|
1365
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
1366
|
+
throw invalidNativeConnectInput("port", "port-range");
|
|
1367
|
+
const serverName = inspectNativeConnectInputField(input, "serverName");
|
|
1368
|
+
const rejectUnauthorized = inspectNativeConnectInputField(input, "rejectUnauthorized");
|
|
1369
|
+
const idleTimeoutMs = inspectNativeConnectInputField(input, "idleTimeoutMs");
|
|
1370
|
+
const timeoutMs = inspectNativeConnectInputField(input, "timeoutMs");
|
|
1371
|
+
const signal = inspectNativeConnectInputField(input, "signal");
|
|
1372
|
+
const affinityKey = inspectNativeConnectInputField(input, "affinityKey");
|
|
1373
|
+
return {
|
|
1374
|
+
host: canonicalHost.host,
|
|
1375
|
+
port,
|
|
1376
|
+
...(serverName === undefined ? {} : { serverName }),
|
|
1377
|
+
...(rejectUnauthorized === undefined ? {} : { rejectUnauthorized }),
|
|
1378
|
+
...(idleTimeoutMs === undefined ? {} : { idleTimeoutMs }),
|
|
1379
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
1380
|
+
...(signal === undefined ? {} : { signal }),
|
|
1381
|
+
...(affinityKey === undefined ? {} : { affinityKey }),
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
790
1385
|
/** Internal canonical snapshot shared by production and SDK transport test doubles. */
|
|
791
1386
|
export function snapshotNativeGrantInput(
|
|
792
1387
|
input: NativeNetworkDynamicGrantOptions,
|
|
793
1388
|
): NativeNetworkDynamicGrantOptions {
|
|
1389
|
+
let snapshot: NativeNetworkDynamicGrantOptions;
|
|
794
1390
|
try {
|
|
795
1391
|
const sourceHost = input.sourceHost;
|
|
796
1392
|
const sourcePort = input.sourcePort;
|
|
@@ -798,7 +1394,7 @@ export function snapshotNativeGrantInput(
|
|
|
798
1394
|
const port = input.port;
|
|
799
1395
|
const tls = input.tls;
|
|
800
1396
|
const ttlMs = input.ttlMs;
|
|
801
|
-
|
|
1397
|
+
snapshot = {
|
|
802
1398
|
sourceHost,
|
|
803
1399
|
sourcePort,
|
|
804
1400
|
host,
|
|
@@ -812,6 +1408,12 @@ export function snapshotNativeGrantInput(
|
|
|
812
1408
|
"native_egress_input_invalid",
|
|
813
1409
|
);
|
|
814
1410
|
}
|
|
1411
|
+
assertValidGrantInput(snapshot);
|
|
1412
|
+
return {
|
|
1413
|
+
...snapshot,
|
|
1414
|
+
sourceHost: canonicalGrantHost(snapshot.sourceHost),
|
|
1415
|
+
host: canonicalGrantHost(snapshot.host),
|
|
1416
|
+
};
|
|
815
1417
|
}
|
|
816
1418
|
|
|
817
1419
|
/** Internal authorization seam shared by production and SDK transport test doubles. */
|
|
@@ -869,20 +1471,21 @@ export function createNativeEgressAuthorization(options: NativeNetworkClientOpti
|
|
|
869
1471
|
|
|
870
1472
|
const assertConnect = (input: NativeNetworkConnectInput, tls: NativeConnectTls): void => {
|
|
871
1473
|
if (!declared) return;
|
|
872
|
-
const host = normalizeEgressHost(input.host);
|
|
873
1474
|
const now = Date.now();
|
|
874
1475
|
purgeInactive(now);
|
|
875
1476
|
if (
|
|
876
1477
|
staticRules.some(
|
|
877
1478
|
(rule) =>
|
|
878
|
-
rule.host ===
|
|
1479
|
+
rule.host === input.host &&
|
|
1480
|
+
rule.ports.includes(input.port) &&
|
|
1481
|
+
tlsModeAllows(rule.tls, tls),
|
|
879
1482
|
)
|
|
880
1483
|
)
|
|
881
1484
|
return;
|
|
882
1485
|
const matching = grants.filter(
|
|
883
1486
|
(grant) =>
|
|
884
1487
|
!grant.revoked &&
|
|
885
|
-
grant.host === host &&
|
|
1488
|
+
grant.host === input.host &&
|
|
886
1489
|
grant.port === input.port &&
|
|
887
1490
|
tlsModeAllows(grant.tls, tls),
|
|
888
1491
|
);
|
|
@@ -890,9 +1493,7 @@ export function createNativeEgressAuthorization(options: NativeNetworkClientOpti
|
|
|
890
1493
|
const expired = [...expiredEvidence.values()]
|
|
891
1494
|
.filter(
|
|
892
1495
|
(grant) =>
|
|
893
|
-
grant.host === host &&
|
|
894
|
-
grant.port === input.port &&
|
|
895
|
-
tlsModeAllows(grant.tls, tls),
|
|
1496
|
+
grant.host === input.host && grant.port === input.port && tlsModeAllows(grant.tls, tls),
|
|
896
1497
|
)
|
|
897
1498
|
.sort((left, right) => (right.expiresAtMs ?? 0) - (left.expiresAtMs ?? 0))[0];
|
|
898
1499
|
if (expired?.expiresAtMs !== undefined)
|
|
@@ -908,11 +1509,45 @@ export function createNativeEgressAuthorization(options: NativeNetworkClientOpti
|
|
|
908
1509
|
const grantLocal = (input: NativeNetworkDynamicGrantOptions): NativeNetworkEgressGrant => {
|
|
909
1510
|
assertValidGrantInput(input);
|
|
910
1511
|
const ruleIndex = dynamicRules.findIndex((rule) => matchesDynamicRuleSelectors(rule, input));
|
|
911
|
-
if (ruleIndex < 0)
|
|
1512
|
+
if (ruleIndex < 0) {
|
|
1513
|
+
const diagnosticSourceHost = safeDiagnosticEgressHost(input.sourceHost);
|
|
1514
|
+
const diagnosticTargetHost = safeDiagnosticEgressHost(input.host);
|
|
1515
|
+
const sourceMatchingRuleIndices = dynamicRules.flatMap((rule, index) =>
|
|
1516
|
+
matchesSourceHost(rule, input.sourceHost) &&
|
|
1517
|
+
matchesPortSelectors(input.sourcePort, rule.sourcePorts, rule.sourcePortRanges)
|
|
1518
|
+
? [index]
|
|
1519
|
+
: [],
|
|
1520
|
+
);
|
|
1521
|
+
const targetKind = classifyEgressHost(diagnosticTargetHost);
|
|
1522
|
+
let selectorDetails: string;
|
|
1523
|
+
if (sourceMatchingRuleIndices.length > 0) {
|
|
1524
|
+
const failedByRule: string[] = [];
|
|
1525
|
+
for (const index of sourceMatchingRuleIndices) {
|
|
1526
|
+
const rule = dynamicRules[index];
|
|
1527
|
+
if (!rule) continue;
|
|
1528
|
+
const failedDimensions: string[] = [];
|
|
1529
|
+
if (!matchesDynamicTargetHost(rule, input.host)) failedDimensions.push("target-host");
|
|
1530
|
+
if (!matchesPortSelectors(input.port, rule.targetPorts, rule.targetPortRanges))
|
|
1531
|
+
failedDimensions.push("target-port");
|
|
1532
|
+
if (!grantTlsFitsRule(input.tls, rule.tls)) failedDimensions.push("tls");
|
|
1533
|
+
failedByRule.push(`rule ${index}: ${failedDimensions.join(", ")}`);
|
|
1534
|
+
}
|
|
1535
|
+
selectorDetails = `source-matching rule indices: [${sourceMatchingRuleIndices.join(", ")}]; failed selector dimensions by rule: ${failedByRule.join("; ")}`;
|
|
1536
|
+
} else {
|
|
1537
|
+
const failedByRule = dynamicRules.map((rule, index) => {
|
|
1538
|
+
const failedDimensions: string[] = [];
|
|
1539
|
+
if (!matchesSourceHost(rule, input.sourceHost)) failedDimensions.push("source-host");
|
|
1540
|
+
if (!matchesPortSelectors(input.sourcePort, rule.sourcePorts, rule.sourcePortRanges))
|
|
1541
|
+
failedDimensions.push("source-port");
|
|
1542
|
+
return `rule ${index}: ${failedDimensions.join(", ")}`;
|
|
1543
|
+
});
|
|
1544
|
+
selectorDetails = `source-matching rule indices: []; failed selector dimensions by rule: ${failedByRule.join("; ")}`;
|
|
1545
|
+
}
|
|
912
1546
|
throw new NativeNetworkError(
|
|
913
|
-
`Native TCP egress grant is not declared for ${input.
|
|
1547
|
+
`Native TCP egress grant is not declared for source ${diagnosticSourceHost}:${input.sourcePort} to target ${diagnosticTargetHost}:${input.port} (${input.tls}); target kind: ${targetKind}; ${selectorDetails}`,
|
|
914
1548
|
"native_egress_not_declared",
|
|
915
1549
|
);
|
|
1550
|
+
}
|
|
916
1551
|
const rule = dynamicRules[ruleIndex];
|
|
917
1552
|
if (!rule) throw invalidGrant("Native TCP egress declaration is missing its matched rule");
|
|
918
1553
|
if (input.ttlMs !== undefined && rule.ttlMs !== undefined && input.ttlMs > rule.ttlMs)
|
|
@@ -920,7 +1555,6 @@ export function createNativeEgressAuthorization(options: NativeNetworkClientOpti
|
|
|
920
1555
|
`Native TCP egress grant ttlMs ${input.ttlMs} exceeds declared maximum ${rule.ttlMs}`,
|
|
921
1556
|
);
|
|
922
1557
|
const now = Date.now();
|
|
923
|
-
const targetHost = normalizeEgressHost(input.host);
|
|
924
1558
|
purgeInactive(now);
|
|
925
1559
|
const activeForRule = grants.filter(
|
|
926
1560
|
(grant) =>
|
|
@@ -939,7 +1573,7 @@ export function createNativeEgressAuthorization(options: NativeNetworkClientOpti
|
|
|
939
1573
|
throw invalidGrant("Native TCP egress grant expiry exceeds the supported date range");
|
|
940
1574
|
const stored: StoredEgressGrant = {
|
|
941
1575
|
ruleIndex,
|
|
942
|
-
host:
|
|
1576
|
+
host: input.host,
|
|
943
1577
|
port: input.port,
|
|
944
1578
|
tls: input.tls,
|
|
945
1579
|
...(expiresAtMs === undefined ? {} : { expiresAtMs }),
|
|
@@ -1023,10 +1657,10 @@ export function createNativeNetworkClient(
|
|
|
1023
1657
|
egress.assertConnect(request, "disabled");
|
|
1024
1658
|
const deadline = deadlineFrom(request.timeoutMs);
|
|
1025
1659
|
assertCanStart(request.signal, deadline);
|
|
1026
|
-
const proxy = await resolveConnectionProxy(options, request);
|
|
1660
|
+
const proxy = await resolveConnectionProxy(options, request, deadline);
|
|
1027
1661
|
egress.assertConnect(request, "disabled");
|
|
1028
1662
|
const socket = proxy
|
|
1029
|
-
? await
|
|
1663
|
+
? await connectProxyTunnel(proxy, request, deadline, () =>
|
|
1030
1664
|
egress.assertConnect(request, "disabled"),
|
|
1031
1665
|
)
|
|
1032
1666
|
: await connectPlainSocket(request.host, request.port, request.signal, deadline);
|
|
@@ -1037,10 +1671,10 @@ export function createNativeNetworkClient(
|
|
|
1037
1671
|
egress.assertConnect(request, "required");
|
|
1038
1672
|
const deadline = deadlineFrom(request.timeoutMs);
|
|
1039
1673
|
assertCanStart(request.signal, deadline);
|
|
1040
|
-
const proxy = await resolveConnectionProxy(options, request);
|
|
1674
|
+
const proxy = await resolveConnectionProxy(options, request, deadline);
|
|
1041
1675
|
egress.assertConnect(request, "required");
|
|
1042
1676
|
const tunnel = proxy
|
|
1043
|
-
? await
|
|
1677
|
+
? await connectProxyTunnel(proxy, request, deadline, () =>
|
|
1044
1678
|
egress.assertConnect(request, "required"),
|
|
1045
1679
|
)
|
|
1046
1680
|
: undefined;
|