@apifuse/provider-sdk 2.2.0-beta.12 → 2.2.0-beta.13
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 +201 -0
- package/CHANGELOG.md +10 -0
- package/README.md +26 -2
- package/bin/apifuse-pack-types.ts +30 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -0
- package/dist/runtime/http.js +86 -32
- package/dist/runtime/instrumentation.js +295 -9
- package/dist/runtime/native-network.d.ts +53 -0
- package/dist/runtime/native-network.js +477 -0
- package/dist/runtime/proxy-nodemaven.d.ts +14 -0
- package/dist/runtime/proxy-nodemaven.js +20 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/stealth.d.ts +3 -1
- package/dist/runtime/stealth.js +239 -39
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +451 -19
- package/dist/types.d.ts +162 -0
- package/package.json +2 -1
- package/src/define.ts +81 -3
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +37 -0
- package/src/provider.ts +37 -0
- package/src/runtime/http.ts +144 -38
- package/src/runtime/instrumentation.ts +424 -8
- package/src/runtime/native-network.ts +600 -0
- package/src/runtime/proxy-nodemaven.ts +37 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/stealth.ts +293 -40
- package/src/server/index.ts +4 -1
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/stream-evidence.ts +988 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run.ts +608 -12
- package/src/types.ts +194 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Socket } from "node:net";
|
|
3
|
+
import { connect as connectTlsSocket } from "node:tls";
|
|
4
|
+
import { SocksClient } from "socks";
|
|
5
|
+
import { ProxyResolutionError } from "../config/loader.js";
|
|
6
|
+
import { TransportError } from "../errors.js";
|
|
7
|
+
import { hasNodemavenCredentials, nodemavenSessionWindow, synthesizeNodemavenProxy, } from "./proxy-nodemaven.js";
|
|
8
|
+
export class NativeNetworkError extends TransportError {
|
|
9
|
+
constructor(message, code) {
|
|
10
|
+
super(message, { code, status: 0 });
|
|
11
|
+
this.name = "NativeNetworkError";
|
|
12
|
+
}
|
|
13
|
+
get code() {
|
|
14
|
+
return super.code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class NativeProxyExpiredError extends NativeNetworkError {
|
|
18
|
+
expiresAt;
|
|
19
|
+
constructor(expiresAt) {
|
|
20
|
+
super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
|
|
21
|
+
this.expiresAt = expiresAt;
|
|
22
|
+
this.name = "NativeProxyExpiredError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
26
|
+
export class NativeIdleTimeoutError extends NativeNetworkError {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("Native network socket timed out while reading.", "native_connection_idle_timeout");
|
|
29
|
+
this.name = "NativeIdleTimeoutError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function synthesizeNodemavenGateway(input) {
|
|
33
|
+
if (input.vendor !== "nodemaven" || !hasNodemavenCredentials())
|
|
34
|
+
return undefined;
|
|
35
|
+
// NodeMaven defaults ctx.http to HTTP CONNECT because SOCKS5 adds ~500ms per
|
|
36
|
+
// request. Native LOCO pays this once per long-lived connection and must reach
|
|
37
|
+
// arbitrary destination ports, so SOCKS5 is mandatory here.
|
|
38
|
+
const sessionWindow = nodemavenSessionWindow(input.policy, input.now);
|
|
39
|
+
const synthesized = synthesizeNodemavenProxy({
|
|
40
|
+
policy: input.policy,
|
|
41
|
+
affinityKey: input.affinityKey,
|
|
42
|
+
protocol: "socks5",
|
|
43
|
+
poolIndex: 0,
|
|
44
|
+
refreshEpoch: sessionWindow.refreshEpoch,
|
|
45
|
+
now: input.now,
|
|
46
|
+
expiresAt: sessionWindow.expiresAt,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
url: synthesized.url,
|
|
50
|
+
vendor: "nodemaven",
|
|
51
|
+
sticky: synthesized.sticky,
|
|
52
|
+
sessionId: synthesized.sessionId,
|
|
53
|
+
expiresAt: synthesized.expiresAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const DEFAULT_GATEWAY_SYNTHESIZERS = [
|
|
57
|
+
synthesizeNodemavenGateway,
|
|
58
|
+
];
|
|
59
|
+
/** Domain-separated, process-independent affinity derived from credential identity. */
|
|
60
|
+
export function deriveNativeCredentialAffinityKey(credentialIdentity) {
|
|
61
|
+
return createHash("sha256")
|
|
62
|
+
.update("apifuse-native-proxy-affinity:v1\0")
|
|
63
|
+
.update(credentialIdentity)
|
|
64
|
+
.digest("hex");
|
|
65
|
+
}
|
|
66
|
+
function isStickyPolicy(policy) {
|
|
67
|
+
return (policy.session?.affinity ?? "request") !== "request";
|
|
68
|
+
}
|
|
69
|
+
function resolveNativeVendorChain(policy) {
|
|
70
|
+
const declared = policy.providers?.length
|
|
71
|
+
? policy.providers
|
|
72
|
+
: policy.provider
|
|
73
|
+
? [policy.provider]
|
|
74
|
+
: ["nodemaven"];
|
|
75
|
+
const chain = [];
|
|
76
|
+
for (const vendor of declared) {
|
|
77
|
+
if (!chain.includes(vendor))
|
|
78
|
+
chain.push(vendor);
|
|
79
|
+
}
|
|
80
|
+
return chain;
|
|
81
|
+
}
|
|
82
|
+
/** Resolve the first configured native gateway without invoking an allocator API. */
|
|
83
|
+
export function resolveNativeGatewayProxy(input) {
|
|
84
|
+
if (input.policy.mode === "disabled")
|
|
85
|
+
return undefined;
|
|
86
|
+
const synthesizers = input.gatewaySynthesizers ?? DEFAULT_GATEWAY_SYNTHESIZERS;
|
|
87
|
+
const now = input.now ?? Date.now();
|
|
88
|
+
for (const vendor of resolveNativeVendorChain(input.policy)) {
|
|
89
|
+
for (const synthesize of synthesizers) {
|
|
90
|
+
const resolved = synthesize({
|
|
91
|
+
vendor,
|
|
92
|
+
policy: input.policy,
|
|
93
|
+
affinityKey: input.affinityKey,
|
|
94
|
+
now,
|
|
95
|
+
});
|
|
96
|
+
if (resolved && resolved.vendor === vendor)
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function proxyRequiredError(policy) {
|
|
103
|
+
const chain = resolveNativeVendorChain(policy).filter((vendor) => vendor === "smartproxy" || vendor === "nodemaven");
|
|
104
|
+
return new ProxyResolutionError("PROXY_REQUIRED", `Native proxy egress is required but no gateway vendor in [${chain.join(", ") || "none"}] resolved.`, { vendorChain: chain });
|
|
105
|
+
}
|
|
106
|
+
function deadlineFrom(timeoutMs) {
|
|
107
|
+
if (timeoutMs === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
return Date.now() + Math.max(0, timeoutMs);
|
|
110
|
+
}
|
|
111
|
+
function remainingMs(deadline) {
|
|
112
|
+
return deadline === undefined ? undefined : Math.max(0, deadline - Date.now());
|
|
113
|
+
}
|
|
114
|
+
function abortError() {
|
|
115
|
+
return new NativeNetworkError("Native connection was aborted", "native_connection_aborted");
|
|
116
|
+
}
|
|
117
|
+
function timeoutError() {
|
|
118
|
+
return new NativeNetworkError("Native connection timed out", "native_connection_timeout");
|
|
119
|
+
}
|
|
120
|
+
function failedError() {
|
|
121
|
+
return new NativeNetworkError("Native connection failed", "native_connection_failed");
|
|
122
|
+
}
|
|
123
|
+
function assertCanStart(signal, deadline) {
|
|
124
|
+
if (signal?.aborted)
|
|
125
|
+
throw abortError();
|
|
126
|
+
if (deadline !== undefined && deadline <= Date.now())
|
|
127
|
+
throw timeoutError();
|
|
128
|
+
}
|
|
129
|
+
async function waitForSocketEvent(socket, event, signal, deadline) {
|
|
130
|
+
assertCanStart(signal, deadline);
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
let timer;
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
if (timer)
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
socket.off(event, onReady);
|
|
137
|
+
socket.off("error", onError);
|
|
138
|
+
socket.off("close", onClose);
|
|
139
|
+
signal?.removeEventListener("abort", onAbort);
|
|
140
|
+
};
|
|
141
|
+
const finish = (error) => {
|
|
142
|
+
cleanup();
|
|
143
|
+
if (error) {
|
|
144
|
+
socket.on("error", () => undefined);
|
|
145
|
+
socket.destroy();
|
|
146
|
+
reject(error);
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
resolve();
|
|
150
|
+
};
|
|
151
|
+
const onReady = () => finish();
|
|
152
|
+
const onError = () => finish(failedError());
|
|
153
|
+
const onClose = () => finish(failedError());
|
|
154
|
+
const onAbort = () => finish(abortError());
|
|
155
|
+
socket.once(event, onReady);
|
|
156
|
+
socket.once("error", onError);
|
|
157
|
+
socket.once("close", onClose);
|
|
158
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
159
|
+
const remaining = remainingMs(deadline);
|
|
160
|
+
if (remaining !== undefined)
|
|
161
|
+
timer = setTimeout(() => finish(timeoutError()), remaining);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function connectPlainSocket(host, port, signal, deadline) {
|
|
165
|
+
assertCanStart(signal, deadline);
|
|
166
|
+
const socket = new Socket();
|
|
167
|
+
const ready = waitForSocketEvent(socket, "connect", signal, deadline);
|
|
168
|
+
socket.connect({ host, port });
|
|
169
|
+
await ready;
|
|
170
|
+
return socket;
|
|
171
|
+
}
|
|
172
|
+
function parseSocks5Proxy(proxyUrl) {
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = new URL(proxyUrl);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
179
|
+
}
|
|
180
|
+
const port = Number(parsed.port);
|
|
181
|
+
if (parsed.protocol !== "socks5:" || !parsed.hostname || !Number.isInteger(port) || port <= 0) {
|
|
182
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return {
|
|
186
|
+
host: parsed.hostname,
|
|
187
|
+
port,
|
|
188
|
+
...(parsed.username ? { userId: decodeURIComponent(parsed.username) } : {}),
|
|
189
|
+
...(parsed.password ? { password: decodeURIComponent(parsed.password) } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new NativeNetworkError("Native proxy URL is invalid", "native_proxy_invalid");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function waitForSocksHandshake(proxySocket, promise, signal, deadline) {
|
|
197
|
+
assertCanStart(signal, deadline);
|
|
198
|
+
return await new Promise((resolve, reject) => {
|
|
199
|
+
let settled = false;
|
|
200
|
+
let timer;
|
|
201
|
+
const cleanup = () => {
|
|
202
|
+
if (timer)
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
signal?.removeEventListener("abort", onAbort);
|
|
205
|
+
};
|
|
206
|
+
const finish = (error, socket) => {
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
210
|
+
cleanup();
|
|
211
|
+
if (error) {
|
|
212
|
+
proxySocket.destroy();
|
|
213
|
+
reject(error);
|
|
214
|
+
}
|
|
215
|
+
else if (socket)
|
|
216
|
+
resolve(socket);
|
|
217
|
+
else
|
|
218
|
+
reject(failedError());
|
|
219
|
+
};
|
|
220
|
+
const onAbort = () => finish(abortError());
|
|
221
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
222
|
+
const remaining = remainingMs(deadline);
|
|
223
|
+
if (remaining !== undefined)
|
|
224
|
+
timer = setTimeout(() => finish(timeoutError()), remaining);
|
|
225
|
+
void promise.then((result) => finish(undefined, result.socket), (error) => finish(error instanceof Error && /\b(?:timed out|timeout)\b/i.test(error.message)
|
|
226
|
+
? timeoutError()
|
|
227
|
+
: failedError()));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function connectSocksTunnel(proxy, input, deadline) {
|
|
231
|
+
const parsed = parseSocks5Proxy(proxy.url);
|
|
232
|
+
const proxySocket = await connectPlainSocket(parsed.host, parsed.port, input.signal, deadline);
|
|
233
|
+
// `socks` removes its internal listeners as the handshake settles. Keep a
|
|
234
|
+
// credential-free sink on the owned socket so an abort/close race cannot emit
|
|
235
|
+
// an unhandled late network error between library cleanup and our wrapper.
|
|
236
|
+
proxySocket.on("error", () => undefined);
|
|
237
|
+
const remaining = remainingMs(deadline);
|
|
238
|
+
const handshake = SocksClient.createConnection({
|
|
239
|
+
command: "connect",
|
|
240
|
+
destination: { host: input.host, port: input.port },
|
|
241
|
+
proxy: { type: 5, ...parsed },
|
|
242
|
+
existing_socket: proxySocket,
|
|
243
|
+
...(remaining === undefined ? {} : { timeout: Math.max(1, remaining) }),
|
|
244
|
+
});
|
|
245
|
+
return await waitForSocksHandshake(proxySocket, handshake, input.signal, deadline);
|
|
246
|
+
}
|
|
247
|
+
async function upgradeTls(socket, input, deadline) {
|
|
248
|
+
assertCanStart(input.signal, deadline);
|
|
249
|
+
const tlsSocket = socket
|
|
250
|
+
? connectTlsSocket({
|
|
251
|
+
socket,
|
|
252
|
+
servername: input.serverName ?? input.host,
|
|
253
|
+
rejectUnauthorized: input.rejectUnauthorized,
|
|
254
|
+
})
|
|
255
|
+
: connectTlsSocket({
|
|
256
|
+
host: input.host,
|
|
257
|
+
port: input.port,
|
|
258
|
+
servername: input.serverName ?? input.host,
|
|
259
|
+
rejectUnauthorized: input.rejectUnauthorized,
|
|
260
|
+
});
|
|
261
|
+
await waitForSocketEvent(tlsSocket, "secureConnect", input.signal, deadline);
|
|
262
|
+
return tlsSocket;
|
|
263
|
+
}
|
|
264
|
+
export function createNativeNetworkConnection(socket, proxy, options, idleTimeoutMs) {
|
|
265
|
+
const warn = options.warn ?? console.warn;
|
|
266
|
+
let terminalError;
|
|
267
|
+
let closeReason;
|
|
268
|
+
let drainHandler;
|
|
269
|
+
let idleTimer;
|
|
270
|
+
let expiringTimer;
|
|
271
|
+
let hardExpiryTimer;
|
|
272
|
+
let lifecycleSettled = false;
|
|
273
|
+
let settleLifecycle = () => undefined;
|
|
274
|
+
const lifecycleCutoff = new Promise((resolve) => {
|
|
275
|
+
settleLifecycle = resolve;
|
|
276
|
+
});
|
|
277
|
+
const clearIdleTimer = () => {
|
|
278
|
+
if (idleTimer)
|
|
279
|
+
clearTimeout(idleTimer);
|
|
280
|
+
idleTimer = undefined;
|
|
281
|
+
};
|
|
282
|
+
const resetIdleTimer = () => {
|
|
283
|
+
clearIdleTimer();
|
|
284
|
+
if (idleTimeoutMs === undefined || socket.readableEnded || socket.destroyed)
|
|
285
|
+
return;
|
|
286
|
+
idleTimer = setTimeout(() => {
|
|
287
|
+
idleTimer = undefined;
|
|
288
|
+
if (socket.readableEnded || socket.destroyed)
|
|
289
|
+
return;
|
|
290
|
+
closeReason = new NativeIdleTimeoutError();
|
|
291
|
+
socket.destroy(closeReason);
|
|
292
|
+
}, Math.max(0, idleTimeoutMs));
|
|
293
|
+
idleTimer.unref?.();
|
|
294
|
+
};
|
|
295
|
+
const clearLifecycle = () => {
|
|
296
|
+
clearIdleTimer();
|
|
297
|
+
if (lifecycleSettled)
|
|
298
|
+
return;
|
|
299
|
+
lifecycleSettled = true;
|
|
300
|
+
if (expiringTimer)
|
|
301
|
+
clearTimeout(expiringTimer);
|
|
302
|
+
if (hardExpiryTimer)
|
|
303
|
+
clearTimeout(hardExpiryTimer);
|
|
304
|
+
settleLifecycle();
|
|
305
|
+
};
|
|
306
|
+
socket.on("error", (error) => {
|
|
307
|
+
terminalError = error;
|
|
308
|
+
});
|
|
309
|
+
socket.once("end", clearIdleTimer);
|
|
310
|
+
socket.once("close", clearLifecycle);
|
|
311
|
+
resetIdleTimer();
|
|
312
|
+
const expiresAt = proxy?.sticky ? proxy.expiresAt : undefined;
|
|
313
|
+
const leadSeconds = options.proxyPolicy?.session?.drainLeadSeconds;
|
|
314
|
+
const expiresAtMs = expiresAt ? Date.parse(expiresAt) : Number.NaN;
|
|
315
|
+
if (expiresAt &&
|
|
316
|
+
Number.isFinite(expiresAtMs) &&
|
|
317
|
+
typeof leadSeconds === "number" &&
|
|
318
|
+
Number.isFinite(leadSeconds) &&
|
|
319
|
+
leadSeconds >= 0) {
|
|
320
|
+
const event = {
|
|
321
|
+
expiresAt,
|
|
322
|
+
leadSeconds,
|
|
323
|
+
reason: "sticky_expiry",
|
|
324
|
+
};
|
|
325
|
+
const remaining = Math.max(0, expiresAtMs - Date.now());
|
|
326
|
+
const leadDelay = Math.max(0, remaining - leadSeconds * 1_000);
|
|
327
|
+
expiringTimer = setTimeout(() => {
|
|
328
|
+
expiringTimer = undefined;
|
|
329
|
+
const handler = drainHandler;
|
|
330
|
+
if (!handler) {
|
|
331
|
+
warn("Native sticky proxy is expiring without a drain handler");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
void (async () => {
|
|
335
|
+
try {
|
|
336
|
+
await Promise.race([Promise.resolve(handler(event)), lifecycleCutoff]);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Drain failures do not bypass the hard-expiry fail-safe.
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
}, leadDelay);
|
|
343
|
+
expiringTimer.unref?.();
|
|
344
|
+
hardExpiryTimer = setTimeout(() => {
|
|
345
|
+
hardExpiryTimer = undefined;
|
|
346
|
+
if (socket.destroyed)
|
|
347
|
+
return;
|
|
348
|
+
closeReason = new NativeProxyExpiredError(expiresAt);
|
|
349
|
+
settleLifecycle();
|
|
350
|
+
socket.destroy();
|
|
351
|
+
}, remaining);
|
|
352
|
+
hardExpiryTimer.unref?.();
|
|
353
|
+
}
|
|
354
|
+
const read = async () => {
|
|
355
|
+
if (closeReason)
|
|
356
|
+
throw closeReason;
|
|
357
|
+
if (terminalError)
|
|
358
|
+
throw failedError();
|
|
359
|
+
const chunk = socket.read();
|
|
360
|
+
if (chunk) {
|
|
361
|
+
resetIdleTimer();
|
|
362
|
+
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
363
|
+
}
|
|
364
|
+
if (socket.readableEnded || socket.destroyed)
|
|
365
|
+
return null;
|
|
366
|
+
await new Promise((resolve) => {
|
|
367
|
+
const cleanup = () => {
|
|
368
|
+
socket.off("readable", onReadable);
|
|
369
|
+
socket.off("end", onDone);
|
|
370
|
+
socket.off("close", onDone);
|
|
371
|
+
socket.off("error", onDone);
|
|
372
|
+
};
|
|
373
|
+
const onReadable = () => {
|
|
374
|
+
cleanup();
|
|
375
|
+
resolve();
|
|
376
|
+
};
|
|
377
|
+
const onDone = () => {
|
|
378
|
+
cleanup();
|
|
379
|
+
resolve();
|
|
380
|
+
};
|
|
381
|
+
socket.once("readable", onReadable);
|
|
382
|
+
socket.once("end", onDone);
|
|
383
|
+
socket.once("close", onDone);
|
|
384
|
+
socket.once("error", onDone);
|
|
385
|
+
});
|
|
386
|
+
return await read();
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
get closeReason() {
|
|
390
|
+
return closeReason;
|
|
391
|
+
},
|
|
392
|
+
...(proxy
|
|
393
|
+
? {
|
|
394
|
+
proxy: {
|
|
395
|
+
vendor: proxy.vendor,
|
|
396
|
+
sticky: proxy.sticky,
|
|
397
|
+
sessionId: proxy.sessionId,
|
|
398
|
+
expiresAt: proxy.expiresAt,
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
: {}),
|
|
402
|
+
read,
|
|
403
|
+
onExpiring: (handler) => {
|
|
404
|
+
drainHandler = handler;
|
|
405
|
+
},
|
|
406
|
+
write: async (data) => {
|
|
407
|
+
if (closeReason)
|
|
408
|
+
throw closeReason;
|
|
409
|
+
if (socket.destroyed) {
|
|
410
|
+
throw new NativeNetworkError("Native connection is closed", "native_connection_closed");
|
|
411
|
+
}
|
|
412
|
+
await new Promise((resolve, reject) => {
|
|
413
|
+
socket.write(data, (error) => {
|
|
414
|
+
if (error)
|
|
415
|
+
reject(failedError());
|
|
416
|
+
else
|
|
417
|
+
resolve();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
close: async () => {
|
|
422
|
+
if (socket.closed || socket.destroyed) {
|
|
423
|
+
clearLifecycle();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await new Promise((resolve) => {
|
|
427
|
+
socket.once("close", () => resolve());
|
|
428
|
+
socket.destroy();
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async function resolveConnectionProxy(options, input) {
|
|
434
|
+
const policy = options.proxyPolicy;
|
|
435
|
+
if (!policy || policy.mode === "disabled")
|
|
436
|
+
return undefined;
|
|
437
|
+
const explicitAffinityKey = input.affinityKey ?? options.affinityKey;
|
|
438
|
+
const affinityKey = explicitAffinityKey ??
|
|
439
|
+
(isStickyPolicy(policy) && options.credentialIdentity !== undefined
|
|
440
|
+
? deriveNativeCredentialAffinityKey(options.credentialIdentity)
|
|
441
|
+
: undefined);
|
|
442
|
+
const resolved = resolveNativeGatewayProxy({
|
|
443
|
+
policy,
|
|
444
|
+
affinityKey,
|
|
445
|
+
gatewaySynthesizers: options.gatewaySynthesizers,
|
|
446
|
+
});
|
|
447
|
+
if (!resolved && policy.mode === "required")
|
|
448
|
+
throw proxyRequiredError(policy);
|
|
449
|
+
return resolved;
|
|
450
|
+
}
|
|
451
|
+
/** Create the SDK byte-stream runtime; deployment egress authorization stays delegated. */
|
|
452
|
+
export function createNativeNetworkClient(options = {}) {
|
|
453
|
+
return {
|
|
454
|
+
connectTcp: async (input) => {
|
|
455
|
+
const deadline = deadlineFrom(input.timeoutMs);
|
|
456
|
+
assertCanStart(input.signal, deadline);
|
|
457
|
+
const proxy = await resolveConnectionProxy(options, input);
|
|
458
|
+
const socket = proxy
|
|
459
|
+
? await connectSocksTunnel(proxy, input, deadline)
|
|
460
|
+
: await connectPlainSocket(input.host, input.port, input.signal, deadline);
|
|
461
|
+
return createNativeNetworkConnection(socket, proxy, options, input.idleTimeoutMs);
|
|
462
|
+
},
|
|
463
|
+
connectTls: async (input) => {
|
|
464
|
+
const deadline = deadlineFrom(input.timeoutMs);
|
|
465
|
+
assertCanStart(input.signal, deadline);
|
|
466
|
+
const proxy = await resolveConnectionProxy(options, input);
|
|
467
|
+
const tunnel = proxy ? await connectSocksTunnel(proxy, input, deadline) : undefined;
|
|
468
|
+
const socket = await upgradeTls(tunnel, input, deadline);
|
|
469
|
+
return createNativeNetworkConnection(socket, proxy, options, input.idleTimeoutMs);
|
|
470
|
+
},
|
|
471
|
+
grantTcpEgress: (input) => {
|
|
472
|
+
if (options.grantTcpEgress)
|
|
473
|
+
return options.grantTcpEgress(input);
|
|
474
|
+
throw new NativeNetworkError("Dynamic native egress authorization is not configured", "native_dynamic_egress_unsupported");
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
@@ -14,6 +14,13 @@ export declare const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol;
|
|
|
14
14
|
export declare const NODEMAVEN_MAX_POOL_SIZE = 50;
|
|
15
15
|
export declare function hasNodemavenCredentials(): boolean;
|
|
16
16
|
export declare function nodemavenPoolSize(policy: ProviderProxyPolicy): number;
|
|
17
|
+
export declare function nodemavenLifetimeMinutes(policy: ProviderProxyPolicy): number;
|
|
18
|
+
export type NodemavenSessionWindow = {
|
|
19
|
+
refreshEpoch: number;
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
};
|
|
22
|
+
/** Stable wall-clock window shared by every process deriving the same sticky sid. */
|
|
23
|
+
export declare function nodemavenSessionWindow(policy: ProviderProxyPolicy, now?: number): NodemavenSessionWindow;
|
|
17
24
|
export type NodemavenSynthesisInput = {
|
|
18
25
|
policy: ProviderProxyPolicy;
|
|
19
26
|
affinityKey: string | undefined;
|
|
@@ -22,10 +29,17 @@ export type NodemavenSynthesisInput = {
|
|
|
22
29
|
refreshEpoch: number;
|
|
23
30
|
/** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
|
|
24
31
|
country?: string;
|
|
32
|
+
/** Clock injection used by native sticky-expiry lifecycle tests. */
|
|
33
|
+
now?: number;
|
|
34
|
+
/** Stable caller-owned hard expiry when the refresh epoch is wall-clock-derived. */
|
|
35
|
+
expiresAt?: string;
|
|
25
36
|
};
|
|
26
37
|
export type NodemavenSynthesis = {
|
|
27
38
|
url: string;
|
|
28
39
|
protocol: ProxyProtocol;
|
|
40
|
+
sticky: boolean;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
expiresAt?: string;
|
|
29
43
|
diagnostics: Record<string, string | number | boolean>;
|
|
30
44
|
};
|
|
31
45
|
/**
|
|
@@ -42,13 +42,22 @@ function resolveNodemavenFilter() {
|
|
|
42
42
|
export function nodemavenPoolSize(policy) {
|
|
43
43
|
return Math.min(NODEMAVEN_MAX_POOL_SIZE, Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)));
|
|
44
44
|
}
|
|
45
|
-
function nodemavenLifetimeMinutes(policy) {
|
|
45
|
+
export function nodemavenLifetimeMinutes(policy) {
|
|
46
46
|
const configured = policy.session?.lifetimeMinutes;
|
|
47
47
|
if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
|
|
48
48
|
return NODEMAVEN_MAX_LIFETIME_MINUTES;
|
|
49
49
|
}
|
|
50
50
|
return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
|
|
51
51
|
}
|
|
52
|
+
/** Stable wall-clock window shared by every process deriving the same sticky sid. */
|
|
53
|
+
export function nodemavenSessionWindow(policy, now = Date.now()) {
|
|
54
|
+
const lifetimeMs = nodemavenLifetimeMinutes(policy) * 60_000;
|
|
55
|
+
const refreshEpoch = Math.floor(now / lifetimeMs);
|
|
56
|
+
return {
|
|
57
|
+
refreshEpoch,
|
|
58
|
+
expiresAt: new Date((refreshEpoch + 1) * lifetimeMs).toISOString(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
52
61
|
/** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
|
|
53
62
|
function slugifyGeo(value) {
|
|
54
63
|
if (!value)
|
|
@@ -97,6 +106,7 @@ export function synthesizeNodemavenProxy(input) {
|
|
|
97
106
|
const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
|
|
98
107
|
const port = selectPort(input.protocol, sid, input.poolIndex);
|
|
99
108
|
const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
|
|
109
|
+
const sticky = isStickyAffinity(input.policy);
|
|
100
110
|
const country = slugifyGeo(input.country ?? input.policy.geo?.country);
|
|
101
111
|
const region = slugifyGeo(input.policy.geo?.subdivision);
|
|
102
112
|
const city = slugifyGeo(input.policy.geo?.city);
|
|
@@ -116,10 +126,18 @@ export function synthesizeNodemavenProxy(input) {
|
|
|
116
126
|
return {
|
|
117
127
|
url,
|
|
118
128
|
protocol: input.protocol,
|
|
129
|
+
sticky,
|
|
130
|
+
sessionId: sid,
|
|
131
|
+
...(sticky
|
|
132
|
+
? {
|
|
133
|
+
expiresAt: input.expiresAt ??
|
|
134
|
+
new Date((input.now ?? Date.now()) + lifetimeMinutes * 60_000).toISOString(),
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
119
137
|
diagnostics: {
|
|
120
138
|
vendor: "nodemaven",
|
|
121
139
|
protocol: input.protocol,
|
|
122
|
-
sticky
|
|
140
|
+
sticky,
|
|
123
141
|
filter,
|
|
124
142
|
lifetimeMinutes,
|
|
125
143
|
...(country ? { country } : {}),
|
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
import type { RequestParams } from "../types.js";
|
|
1
|
+
import type { HttpClient, RequestOptions, RequestParams } from "../types.js";
|
|
2
|
+
export declare const REDACTED_QUERY_VALUE = "[REDACTED]";
|
|
3
|
+
/** Internal heuristic shared by diagnostic and fixture structural redaction. */
|
|
4
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
5
|
+
declare const HTTP_REQUEST_METHOD_CONFIG: {
|
|
6
|
+
readonly request: {
|
|
7
|
+
readonly optionsIndex: 1;
|
|
8
|
+
};
|
|
9
|
+
readonly get: {
|
|
10
|
+
readonly optionsIndex: 1;
|
|
11
|
+
};
|
|
12
|
+
readonly post: {
|
|
13
|
+
readonly optionsIndex: 2;
|
|
14
|
+
};
|
|
15
|
+
readonly put: {
|
|
16
|
+
readonly optionsIndex: 2;
|
|
17
|
+
};
|
|
18
|
+
readonly delete: {
|
|
19
|
+
readonly optionsIndex: 1;
|
|
20
|
+
};
|
|
21
|
+
readonly stream: {
|
|
22
|
+
readonly optionsIndex: 1;
|
|
23
|
+
};
|
|
24
|
+
readonly sse: {
|
|
25
|
+
readonly optionsIndex: 1;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export type HttpRequestMethod = keyof typeof HTTP_REQUEST_METHOD_CONFIG;
|
|
29
|
+
export type HttpRequestInvocation = {
|
|
30
|
+
[M in HttpRequestMethod]: {
|
|
31
|
+
method: M;
|
|
32
|
+
args: Parameters<HttpClient[M]>;
|
|
33
|
+
};
|
|
34
|
+
}[HttpRequestMethod];
|
|
35
|
+
export declare const HTTP_REQUEST_METHOD_NAMES: HttpRequestMethod[];
|
|
36
|
+
export declare function isHttpRequestMethod(method: PropertyKey): method is HttpRequestMethod;
|
|
37
|
+
export type SerializedRequestUrl = {
|
|
38
|
+
requestUrl: string;
|
|
39
|
+
redactedUrl: string;
|
|
40
|
+
sensitiveValues: readonly string[];
|
|
41
|
+
};
|
|
42
|
+
export declare function normalizeSensitiveParams(sensitiveParams?: Record<string, string>): Record<string, string> | undefined;
|
|
2
43
|
export declare function appendQueryParams(url: string, params?: RequestParams): string;
|
|
44
|
+
/** Redacts matching query keys without reserializing any other URL bytes. */
|
|
45
|
+
export declare function redactUrlQueryParams(url: string, sensitiveParamNames: readonly string[]): {
|
|
46
|
+
redactedUrl: string;
|
|
47
|
+
sensitiveValues: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Serializes public and secret query parameters together while constructing a
|
|
51
|
+
* separate, structurally redacted URL for diagnostics and observability.
|
|
52
|
+
*/
|
|
53
|
+
export declare function serializeRequestUrl(url: string, params?: RequestParams, sensitiveParams?: Record<string, string>): SerializedRequestUrl;
|
|
54
|
+
/** Scrubs raw and URL-encoded secret values from diagnostic text. */
|
|
55
|
+
export declare function redactSensitiveText(text: string, sensitiveValues: readonly string[], requestUrl?: string, redactedUrl?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Redacts error text and recursively serializable diagnostic metadata.
|
|
58
|
+
* Mutable errors retain identity; readonly/frozen errors may be replaced, so
|
|
59
|
+
* callers must use the returned value.
|
|
60
|
+
*/
|
|
61
|
+
export declare function redactSensitiveError<T>(error: T, sensitiveValues: readonly string[], requestUrl?: string, redactedUrl?: string): T;
|
|
62
|
+
/** Redacts failures that occur before a request URL can be fully serialized. */
|
|
63
|
+
export declare function redactSensitiveRequestError<T>(error: T, url: string, sensitiveParams?: Record<string, string>): T;
|
|
64
|
+
/** Validates an untyped proxy invocation before narrowing it to the HttpClient contract. */
|
|
65
|
+
export declare function parseHttpRequestInvocation(method: PropertyKey, args: unknown[]): HttpRequestInvocation | undefined;
|
|
66
|
+
/** Internal typed registry for the request-options position of HttpClient calls. */
|
|
67
|
+
export declare function requestOptionsFromHttpInvocation(invocation: HttpRequestInvocation): RequestOptions | undefined;
|
|
68
|
+
export declare function replaceRequestOptionsInHttpInvocation(invocation: HttpRequestInvocation, options: RequestOptions): void;
|
|
3
69
|
export declare function normalizeHttpRequestBody(body: unknown): string | Buffer | undefined;
|
|
70
|
+
export {};
|