@openclaw/proxyline 0.3.7 → 0.3.8
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/CHANGELOG.md +9 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +6 -1
- package/dist/node-http.d.ts +2 -0
- package/dist/node-http.d.ts.map +1 -1
- package/dist/node-http.js +163 -35
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -7
- package/docs/surfaces.md +4 -0
- package/package.json +5 -5
- package/src/env.ts +6 -1
- package/src/node-http.ts +193 -37
- package/src/runtime.ts +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.8 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
- Fixed HTTP-forward Node agents to enforce the default 30-second proxy handshake timeout and cancel pending sockets when callers abort or destroy requests; explicit `0` remains unbounded. (#27) Thanks @SebTardif.
|
|
6
|
+
- Fixed Node proxy request hooks to follow the request selected by finite connection pools across origins, keeping queued requests' timeouts and cancellation independent. (#27)
|
|
7
|
+
- Hardened ambient `NO_PROXY` hostname normalization against dot-heavy input, docs table-of-contents text extraction, and proxy/TLS test isolation. (#24) Thanks @vincentkoc.
|
|
8
|
+
- Fixed HTTP-forward agent shutdown to destroy pending proxy sockets and settle requests before the connection handshake completes. (#23) Thanks @SebTardif.
|
|
9
|
+
- Fixed throwing `onEvent` observers to preserve successful installation and leave the proxy runtime stoppable and replaceable. (#23) Thanks @SebTardif.
|
|
10
|
+
- Updated Node types, tsx, ws, and pnpm tooling. (#21, #22)
|
|
11
|
+
|
|
3
12
|
## 0.3.7 - 2026-08-02
|
|
4
13
|
|
|
5
14
|
- Fixed Node HTTP/HTTPS proxy agents to apply the default 30-second CONNECT timeout when callers omit a request timeout; explicit `0` remains unbounded. Thanks @SebTardif.
|
package/dist/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,KAAK,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,eAAe,EAAE,gBAS7B,CAAC;AAEF,wBAAgB,YAAY,IAAI,gBAAgB,CAW/C;AAoBD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,KAAK,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,eAAe,EAAE,gBAS7B,CAAC;AAEF,wBAAgB,YAAY,IAAI,gBAAgB,CAW/C;AAoBD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AA4HD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAmC/E"}
|
package/dist/env.js
CHANGED
|
@@ -103,7 +103,12 @@ function matchesNoProxy(url, env) {
|
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
105
|
function normalizeNoProxyHost(hostname) {
|
|
106
|
-
const
|
|
106
|
+
const value = hostname.trim().toLowerCase();
|
|
107
|
+
let end = value.length;
|
|
108
|
+
while (end > 0 && value[end - 1] === ".") {
|
|
109
|
+
end -= 1;
|
|
110
|
+
}
|
|
111
|
+
const normalized = value.slice(0, end);
|
|
107
112
|
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
108
113
|
? normalized.slice(1, -1)
|
|
109
114
|
: normalized;
|
package/dist/node-http.d.ts
CHANGED
|
@@ -9,8 +9,10 @@ export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions
|
|
|
9
9
|
type NodeHttpMethod = typeof http.request;
|
|
10
10
|
type NodeAgentFactory = (options: NodeHttpRequestOptions) => http.Agent;
|
|
11
11
|
type NodeAgentOptions = http.AgentOptions & https.AgentOptions;
|
|
12
|
+
declare const pendingRequestSymbol: unique symbol;
|
|
12
13
|
type NodeAgentRequestOptions = http.RequestOptions & https.RequestOptions & {
|
|
13
14
|
secureEndpoint?: boolean;
|
|
15
|
+
[pendingRequestSymbol]?: http.ClientRequest;
|
|
14
16
|
};
|
|
15
17
|
type NodeProxyAgentOptions = NodeAgentOptions & {
|
|
16
18
|
defaultProtocol?: "http" | "https";
|
package/dist/node-http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,MAAM,YAAY,CAAC;AAI/B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAmE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG;IAChF,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC;AAC1C,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC,KAAK,CAAC;AACxE,KAAK,gBAAgB,GAAG,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;AAC/D,KAAK,uBAAuB,GAAG,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
|
package/dist/node-http.js
CHANGED
|
@@ -6,6 +6,7 @@ import { domainToASCII } from "node:url";
|
|
|
6
6
|
import { readProxyEnv, resolveAmbientProxyForUrl, } from "./env.js";
|
|
7
7
|
import { formatConnectAuthority, resolveProxyConnectTimeoutMs } from "./connect.js";
|
|
8
8
|
import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa } from "./shared.js";
|
|
9
|
+
const pendingRequestSymbol = Symbol("proxyline.pendingRequest");
|
|
9
10
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
10
11
|
const INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
11
12
|
const INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
@@ -295,9 +296,25 @@ function destinationTlsConnectOptions(options, socket) {
|
|
|
295
296
|
delete tlsOptions.agent;
|
|
296
297
|
return tlsOptions;
|
|
297
298
|
}
|
|
298
|
-
class
|
|
299
|
+
class ProxylineRequestAgent extends http.Agent {
|
|
300
|
+
addRequest(req, options) {
|
|
301
|
+
http.Agent.prototype.addRequest.call(this, req, options);
|
|
302
|
+
}
|
|
303
|
+
createSocket(req, options, callback) {
|
|
304
|
+
// Node chooses queued requests by origin, not global FIFO, and clones options.
|
|
305
|
+
http.Agent.prototype.createSocket.call(this, req, { ...options, [pendingRequestSymbol]: req }, callback);
|
|
306
|
+
}
|
|
307
|
+
takePendingRequest(options) {
|
|
308
|
+
const request = options[pendingRequestSymbol];
|
|
309
|
+
// Node retains these options in pooled socket listeners after handoff.
|
|
310
|
+
delete options[pendingRequestSymbol];
|
|
311
|
+
return request;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
class ProxylineHttpForwardAgent extends ProxylineRequestAgent {
|
|
299
315
|
options;
|
|
300
316
|
#keepAlive;
|
|
317
|
+
#pendingConnectSockets = new Set();
|
|
301
318
|
#proxy;
|
|
302
319
|
#proxyTls;
|
|
303
320
|
constructor(proxy, options, proxyTls) {
|
|
@@ -310,34 +327,170 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
310
327
|
addRequest(req, options) {
|
|
311
328
|
setForwardProxyRequestPath(req, options);
|
|
312
329
|
setProxyRequestHeaders(req, this.#proxy, this.#keepAlive);
|
|
313
|
-
|
|
330
|
+
super.addRequest(req, options);
|
|
314
331
|
}
|
|
315
|
-
createConnection(
|
|
332
|
+
createConnection(options, callback) {
|
|
333
|
+
const request = this.takePendingRequest(options);
|
|
316
334
|
const socket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
317
335
|
// When Node supplies an async callback, deliver the socket only through that
|
|
318
336
|
// path. Returning the same socket as well double-invokes Agent setup and can
|
|
319
337
|
// hand an unready TLS proxy socket to plain HTTP forward traffic.
|
|
320
338
|
if (callback !== undefined) {
|
|
321
|
-
|
|
339
|
+
this.#pendingConnectSockets.add(socket);
|
|
340
|
+
let pendingTimeout;
|
|
341
|
+
let settled = false;
|
|
342
|
+
let originalRequestSetTimeout;
|
|
343
|
+
let hookedRequestSetTimeout;
|
|
344
|
+
let originalRequestDestroy;
|
|
345
|
+
let hookedRequestDestroy;
|
|
346
|
+
const restoreRequestDestroyHook = () => {
|
|
347
|
+
if (request !== undefined &&
|
|
348
|
+
originalRequestDestroy !== undefined &&
|
|
349
|
+
request.destroy === hookedRequestDestroy) {
|
|
350
|
+
request.destroy = originalRequestDestroy;
|
|
351
|
+
}
|
|
352
|
+
originalRequestDestroy = undefined;
|
|
353
|
+
hookedRequestDestroy = undefined;
|
|
354
|
+
};
|
|
355
|
+
const startPendingTimeout = (timeoutMs) => {
|
|
356
|
+
if (pendingTimeout !== undefined) {
|
|
357
|
+
clearTimeout(pendingTimeout);
|
|
358
|
+
}
|
|
359
|
+
pendingTimeout = setTimeout(() => {
|
|
360
|
+
request?.emit("timeout");
|
|
361
|
+
if (!settled) {
|
|
362
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
|
|
363
|
+
}
|
|
364
|
+
}, timeoutMs);
|
|
365
|
+
pendingTimeout.unref?.();
|
|
366
|
+
};
|
|
367
|
+
const clearPendingTimeout = () => {
|
|
368
|
+
if (pendingTimeout !== undefined) {
|
|
369
|
+
clearTimeout(pendingTimeout);
|
|
370
|
+
pendingTimeout = undefined;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const restoreRequestTimeoutHook = () => {
|
|
374
|
+
if (request !== undefined &&
|
|
375
|
+
originalRequestSetTimeout !== undefined &&
|
|
376
|
+
request.setTimeout === hookedRequestSetTimeout) {
|
|
377
|
+
request.setTimeout = originalRequestSetTimeout;
|
|
378
|
+
}
|
|
379
|
+
originalRequestSetTimeout = undefined;
|
|
380
|
+
hookedRequestSetTimeout = undefined;
|
|
381
|
+
};
|
|
382
|
+
const cleanup = () => {
|
|
383
|
+
clearPendingTimeout();
|
|
384
|
+
this.#pendingConnectSockets.delete(socket);
|
|
385
|
+
restoreRequestTimeoutHook();
|
|
386
|
+
restoreRequestDestroyHook();
|
|
387
|
+
socket.off(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
388
|
+
socket.off("error", onError);
|
|
389
|
+
socket.off("close", onClosed);
|
|
390
|
+
request?.off("abort", onRequestClosed);
|
|
391
|
+
request?.off("close", onRequestClosed);
|
|
392
|
+
request?.off("error", onRequestClosed);
|
|
393
|
+
request?.off("timeout", onRequestTimedOut);
|
|
394
|
+
};
|
|
395
|
+
const finish = (error) => {
|
|
396
|
+
if (settled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
settled = true;
|
|
400
|
+
cleanup();
|
|
322
401
|
callback(error, socket);
|
|
323
402
|
};
|
|
403
|
+
const fail = (error) => {
|
|
404
|
+
socket.destroy();
|
|
405
|
+
finish(error);
|
|
406
|
+
};
|
|
407
|
+
const onError = (error) => {
|
|
408
|
+
finish(error);
|
|
409
|
+
};
|
|
410
|
+
const onClosed = () => {
|
|
411
|
+
finish(new ProxylineError("CONNECT_FAILED", "proxy socket closed before connection completed"));
|
|
412
|
+
};
|
|
324
413
|
const onConnected = () => {
|
|
325
|
-
|
|
326
|
-
callback(null, socket);
|
|
414
|
+
finish(null);
|
|
327
415
|
};
|
|
416
|
+
const onRequestTimedOut = () => {
|
|
417
|
+
if (!settled) {
|
|
418
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const onRequestClosed = () => {
|
|
422
|
+
if (!settled) {
|
|
423
|
+
fail(new ProxylineError("CONNECT_FAILED", "request closed before proxy connection completed"));
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
if (request !== undefined) {
|
|
427
|
+
originalRequestSetTimeout = request.setTimeout;
|
|
428
|
+
hookedRequestSetTimeout = function hookedSetTimeout(timeout, callback) {
|
|
429
|
+
const result = originalRequestSetTimeout?.call(this, timeout, callback) ?? this;
|
|
430
|
+
const timeoutMs = normalizedPositiveInteger(timeout);
|
|
431
|
+
if (timeoutMs !== undefined) {
|
|
432
|
+
startPendingTimeout(timeoutMs);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
clearPendingTimeout();
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
};
|
|
439
|
+
request.setTimeout = hookedRequestSetTimeout;
|
|
440
|
+
}
|
|
441
|
+
// Prefer explicit agent/request timeout. Default 30s only when both are omitted.
|
|
442
|
+
// Explicit values go through normalizedPositiveInteger first so fractional/invalid
|
|
443
|
+
// timeouts keep the historical no-pending-timer behavior (not Math.trunc to 1ms).
|
|
444
|
+
const requestTimeout = request?.timeout;
|
|
445
|
+
const rawTimeout = options.timeout !== undefined ? options.timeout : requestTimeout;
|
|
446
|
+
if (rawTimeout === undefined) {
|
|
447
|
+
const connectTimeoutMs = resolveProxyConnectTimeoutMs(undefined);
|
|
448
|
+
if (connectTimeoutMs !== undefined) {
|
|
449
|
+
startPendingTimeout(connectTimeoutMs);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const timeoutMs = normalizedPositiveInteger(rawTimeout);
|
|
454
|
+
if (timeoutMs !== undefined) {
|
|
455
|
+
startPendingTimeout(timeoutMs);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
request?.once("timeout", onRequestTimedOut);
|
|
328
459
|
socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
329
460
|
socket.once("error", onError);
|
|
461
|
+
socket.once("close", onClosed);
|
|
462
|
+
// Node defers ClientRequest.destroy() until the async socket callback.
|
|
463
|
+
if (request !== undefined) {
|
|
464
|
+
originalRequestDestroy = request.destroy;
|
|
465
|
+
hookedRequestDestroy = function hookedDestroy(error) {
|
|
466
|
+
const result = originalRequestDestroy?.call(this, error) ?? this;
|
|
467
|
+
onRequestClosed();
|
|
468
|
+
return result;
|
|
469
|
+
};
|
|
470
|
+
request.destroy = hookedRequestDestroy;
|
|
471
|
+
}
|
|
472
|
+
request?.once("abort", onRequestClosed);
|
|
473
|
+
request?.once("close", onRequestClosed);
|
|
474
|
+
request?.once("error", onRequestClosed);
|
|
475
|
+
if (request?.destroyed) {
|
|
476
|
+
onRequestClosed();
|
|
477
|
+
}
|
|
330
478
|
return undefined;
|
|
331
479
|
}
|
|
332
480
|
return socket;
|
|
333
481
|
}
|
|
482
|
+
destroy() {
|
|
483
|
+
for (const socket of this.#pendingConnectSockets) {
|
|
484
|
+
socket.destroy(new ProxylineError("CONNECT_FAILED", "proxy connection failed because agent was destroyed"));
|
|
485
|
+
}
|
|
486
|
+
this.#pendingConnectSockets.clear();
|
|
487
|
+
super.destroy();
|
|
488
|
+
}
|
|
334
489
|
}
|
|
335
|
-
class ProxylineConnectAgent extends
|
|
490
|
+
class ProxylineConnectAgent extends ProxylineRequestAgent {
|
|
336
491
|
options;
|
|
337
492
|
#keepAlive;
|
|
338
493
|
#pendingConnectSockets = new Set();
|
|
339
|
-
#pendingRequests = new WeakMap();
|
|
340
|
-
#pendingRequestQueue = [];
|
|
341
494
|
#proxy;
|
|
342
495
|
#proxyTls;
|
|
343
496
|
constructor(proxy, options, proxyTls) {
|
|
@@ -347,33 +500,8 @@ class ProxylineConnectAgent extends http.Agent {
|
|
|
347
500
|
this.#proxy = proxy;
|
|
348
501
|
this.#proxyTls = proxyTls;
|
|
349
502
|
}
|
|
350
|
-
addRequest(req, options) {
|
|
351
|
-
this.#pendingRequests.set(options, req);
|
|
352
|
-
this.#pendingRequestQueue.push(req);
|
|
353
|
-
req.once("socket", () => this.#removePendingRequest(req));
|
|
354
|
-
req.once("close", () => this.#removePendingRequest(req));
|
|
355
|
-
try {
|
|
356
|
-
http.Agent.prototype.addRequest.call(this, req, options);
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
this.#pendingRequests.delete(options);
|
|
360
|
-
this.#removePendingRequest(req);
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
#removePendingRequest(req) {
|
|
365
|
-
const index = this.#pendingRequestQueue.indexOf(req);
|
|
366
|
-
if (index !== -1) {
|
|
367
|
-
this.#pendingRequestQueue.splice(index, 1);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
503
|
createConnection(options, callback) {
|
|
371
|
-
const
|
|
372
|
-
const request = mappedRequest ?? this.#pendingRequestQueue.shift();
|
|
373
|
-
this.#pendingRequests.delete(options);
|
|
374
|
-
if (mappedRequest !== undefined) {
|
|
375
|
-
this.#removePendingRequest(mappedRequest);
|
|
376
|
-
}
|
|
504
|
+
const request = this.takePendingRequest(options);
|
|
377
505
|
if (callback === undefined) {
|
|
378
506
|
throw new ProxylineError("INVALID_CONNECT_CALLBACK", "CONNECT agents require an async socket callback.");
|
|
379
507
|
}
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AA00BpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
|
package/dist/runtime.js
CHANGED
|
@@ -145,7 +145,12 @@ function normalizeProxyUrl(value) {
|
|
|
145
145
|
return url;
|
|
146
146
|
}
|
|
147
147
|
function emit(onEvent, event) {
|
|
148
|
-
|
|
148
|
+
try {
|
|
149
|
+
onEvent?.(event);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Observer failures must not fail install or leave patched globals unstoppable.
|
|
153
|
+
}
|
|
149
154
|
}
|
|
150
155
|
function isProxyableUrlProtocol(protocol) {
|
|
151
156
|
return protocol === "http:" ||
|
|
@@ -662,12 +667,6 @@ export function installProxyline(options) {
|
|
|
662
667
|
undici: options.undici,
|
|
663
668
|
})
|
|
664
669
|
: undefined;
|
|
665
|
-
emit(options.onEvent, {
|
|
666
|
-
type: "runtime.installed",
|
|
667
|
-
mode: options.mode,
|
|
668
|
-
active: hasActiveProxy,
|
|
669
|
-
...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
|
|
670
|
-
});
|
|
671
670
|
const handle = {
|
|
672
671
|
mode: options.mode,
|
|
673
672
|
active: hasActiveProxy,
|
|
@@ -725,6 +724,12 @@ export function installProxyline(options) {
|
|
|
725
724
|
},
|
|
726
725
|
};
|
|
727
726
|
activeHandle = hasActiveProxy ? handle : activeHandle;
|
|
727
|
+
emit(options.onEvent, {
|
|
728
|
+
type: "runtime.installed",
|
|
729
|
+
mode: options.mode,
|
|
730
|
+
active: hasActiveProxy,
|
|
731
|
+
...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
|
|
732
|
+
});
|
|
728
733
|
return handle;
|
|
729
734
|
}
|
|
730
735
|
export const installGlobalProxy = installProxyline;
|
package/docs/surfaces.md
CHANGED
|
@@ -22,6 +22,10 @@ Per request, Proxyline:
|
|
|
22
22
|
|
|
23
23
|
This means caller agents are not just ignored — the per-request agent is replaced before the original method runs, so even libraries that read `req.agent` after construction see the proxy agent.
|
|
24
24
|
|
|
25
|
+
HTTP-forward proxy connections and CONNECT handshakes default to a 30-second connection timeout. A request's `timeout` option or `req.setTimeout(ms)` can change the pending handshake timeout; use `0` to leave it unbounded. Expiry emits `timeout` on that request and fails the pending connection with `CONNECT_FAILED`. This applies to the explicit Node helper agents as well as patched requests.
|
|
26
|
+
|
|
27
|
+
For HTTP-forward requests, `req.destroy()` or `req.abort()` also cancels a pending connection to the proxy. This applies to long-lived helper agents: cancellation releases the pending socket without destroying the whole agent or affecting another queued request.
|
|
28
|
+
|
|
25
29
|
### TLS identity preservation
|
|
26
30
|
|
|
27
31
|
When the caller supplied an `https.Agent` with TLS options, the following keys are lifted into the request so the destination TLS handshake still validates correctly:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/proxyline",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Process-global proxy routing for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "https://github.com/openclaw/proxyline/issues"
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://proxyline.dev",
|
|
20
|
-
"packageManager": "pnpm@11.
|
|
20
|
+
"packageManager": "pnpm@11.21.0",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"crabbox:warmup": "crabbox warmup"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@types/node": "26.
|
|
65
|
+
"@types/node": "26.2.0",
|
|
66
66
|
"@types/ws": "8.18.1",
|
|
67
|
-
"tsx": "4.23.
|
|
67
|
+
"tsx": "4.23.12",
|
|
68
68
|
"typescript": "^7.0.2",
|
|
69
69
|
"undici": "8.10.0",
|
|
70
|
-
"ws": "8.21.
|
|
70
|
+
"ws": "8.21.3"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=22.19.0"
|
package/src/env.ts
CHANGED
|
@@ -134,7 +134,12 @@ function matchesNoProxy(url: URL, env: ProxyEnvSnapshot): boolean {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function normalizeNoProxyHost(hostname: string): string {
|
|
137
|
-
const
|
|
137
|
+
const value = hostname.trim().toLowerCase();
|
|
138
|
+
let end = value.length;
|
|
139
|
+
while (end > 0 && value[end - 1] === ".") {
|
|
140
|
+
end -= 1;
|
|
141
|
+
}
|
|
142
|
+
const normalized = value.slice(0, end);
|
|
138
143
|
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
139
144
|
? normalized.slice(1, -1)
|
|
140
145
|
: normalized;
|
package/src/node-http.ts
CHANGED
|
@@ -19,17 +19,27 @@ export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions
|
|
|
19
19
|
type NodeHttpMethod = typeof http.request;
|
|
20
20
|
type NodeAgentFactory = (options: NodeHttpRequestOptions) => http.Agent;
|
|
21
21
|
type NodeAgentOptions = http.AgentOptions & https.AgentOptions;
|
|
22
|
+
const pendingRequestSymbol = Symbol("proxyline.pendingRequest");
|
|
22
23
|
type NodeAgentRequestOptions = http.RequestOptions & https.RequestOptions & {
|
|
23
24
|
secureEndpoint?: boolean;
|
|
25
|
+
[pendingRequestSymbol]?: http.ClientRequest;
|
|
24
26
|
};
|
|
25
27
|
type NodeAddRequestAgent = http.Agent & {
|
|
26
28
|
addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void;
|
|
27
29
|
};
|
|
30
|
+
type NodeCreateSocketAgent = http.Agent & {
|
|
31
|
+
createSocket(
|
|
32
|
+
req: http.ClientRequest,
|
|
33
|
+
options: NodeAgentRequestOptions,
|
|
34
|
+
callback: (error: Error | null, socket?: net.Socket) => void,
|
|
35
|
+
): void;
|
|
36
|
+
};
|
|
28
37
|
type RequestSetTimeout = (
|
|
29
38
|
this: http.ClientRequest,
|
|
30
39
|
timeout: number,
|
|
31
40
|
callback?: () => void,
|
|
32
41
|
) => http.ClientRequest;
|
|
42
|
+
type RequestDestroy = (this: http.ClientRequest, error?: Error) => http.ClientRequest;
|
|
33
43
|
type NodeAgentWithOptions = http.Agent & {
|
|
34
44
|
options?: NodeAgentOptions;
|
|
35
45
|
};
|
|
@@ -417,9 +427,34 @@ function destinationTlsConnectOptions(
|
|
|
417
427
|
return tlsOptions;
|
|
418
428
|
}
|
|
419
429
|
|
|
420
|
-
class
|
|
430
|
+
abstract class ProxylineRequestAgent extends http.Agent {
|
|
431
|
+
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
432
|
+
(http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public createSocket(
|
|
436
|
+
req: http.ClientRequest,
|
|
437
|
+
options: NodeAgentRequestOptions,
|
|
438
|
+
callback: (error: Error | null, socket?: net.Socket) => void,
|
|
439
|
+
): void {
|
|
440
|
+
// Node chooses queued requests by origin, not global FIFO, and clones options.
|
|
441
|
+
(http.Agent.prototype as unknown as NodeCreateSocketAgent).createSocket.call(
|
|
442
|
+
this, req, { ...options, [pendingRequestSymbol]: req }, callback,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
protected takePendingRequest(options: NodeAgentRequestOptions): http.ClientRequest | undefined {
|
|
447
|
+
const request = options[pendingRequestSymbol];
|
|
448
|
+
// Node retains these options in pooled socket listeners after handoff.
|
|
449
|
+
delete options[pendingRequestSymbol];
|
|
450
|
+
return request;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class ProxylineHttpForwardAgent extends ProxylineRequestAgent {
|
|
421
455
|
public readonly options: NodeAgentOptions;
|
|
422
456
|
readonly #keepAlive: boolean;
|
|
457
|
+
readonly #pendingConnectSockets = new Set<net.Socket>();
|
|
423
458
|
readonly #proxy: URL;
|
|
424
459
|
readonly #proxyTls: ProxylineTlsOptions | undefined;
|
|
425
460
|
|
|
@@ -431,42 +466,189 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
431
466
|
this.#proxyTls = proxyTls;
|
|
432
467
|
}
|
|
433
468
|
|
|
434
|
-
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
469
|
+
public override addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
435
470
|
setForwardProxyRequestPath(req as http.ClientRequest & { _header?: string | null }, options);
|
|
436
471
|
setProxyRequestHeaders(req, this.#proxy, this.#keepAlive);
|
|
437
|
-
|
|
472
|
+
super.addRequest(req, options);
|
|
438
473
|
}
|
|
439
474
|
|
|
440
475
|
public override createConnection(
|
|
441
|
-
|
|
476
|
+
options: NodeAgentRequestOptions,
|
|
442
477
|
callback?: (error: Error | null, socket: net.Socket) => void,
|
|
443
478
|
): net.Socket {
|
|
479
|
+
const request = this.takePendingRequest(options);
|
|
444
480
|
const socket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
445
481
|
// When Node supplies an async callback, deliver the socket only through that
|
|
446
482
|
// path. Returning the same socket as well double-invokes Agent setup and can
|
|
447
483
|
// hand an unready TLS proxy socket to plain HTTP forward traffic.
|
|
448
484
|
if (callback !== undefined) {
|
|
449
|
-
|
|
485
|
+
this.#pendingConnectSockets.add(socket);
|
|
486
|
+
let pendingTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
487
|
+
let settled = false;
|
|
488
|
+
let originalRequestSetTimeout: RequestSetTimeout | undefined;
|
|
489
|
+
let hookedRequestSetTimeout: RequestSetTimeout | undefined;
|
|
490
|
+
let originalRequestDestroy: RequestDestroy | undefined;
|
|
491
|
+
let hookedRequestDestroy: RequestDestroy | undefined;
|
|
492
|
+
|
|
493
|
+
const restoreRequestDestroyHook = (): void => {
|
|
494
|
+
if (
|
|
495
|
+
request !== undefined &&
|
|
496
|
+
originalRequestDestroy !== undefined &&
|
|
497
|
+
request.destroy === hookedRequestDestroy
|
|
498
|
+
) {
|
|
499
|
+
request.destroy = originalRequestDestroy;
|
|
500
|
+
}
|
|
501
|
+
originalRequestDestroy = undefined;
|
|
502
|
+
hookedRequestDestroy = undefined;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const startPendingTimeout = (timeoutMs: number): void => {
|
|
506
|
+
if (pendingTimeout !== undefined) {
|
|
507
|
+
clearTimeout(pendingTimeout);
|
|
508
|
+
}
|
|
509
|
+
pendingTimeout = setTimeout(() => {
|
|
510
|
+
request?.emit("timeout");
|
|
511
|
+
if (!settled) {
|
|
512
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
|
|
513
|
+
}
|
|
514
|
+
}, timeoutMs);
|
|
515
|
+
pendingTimeout.unref?.();
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const clearPendingTimeout = (): void => {
|
|
519
|
+
if (pendingTimeout !== undefined) {
|
|
520
|
+
clearTimeout(pendingTimeout);
|
|
521
|
+
pendingTimeout = undefined;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const restoreRequestTimeoutHook = (): void => {
|
|
526
|
+
if (
|
|
527
|
+
request !== undefined &&
|
|
528
|
+
originalRequestSetTimeout !== undefined &&
|
|
529
|
+
request.setTimeout === hookedRequestSetTimeout
|
|
530
|
+
) {
|
|
531
|
+
request.setTimeout = originalRequestSetTimeout;
|
|
532
|
+
}
|
|
533
|
+
originalRequestSetTimeout = undefined;
|
|
534
|
+
hookedRequestSetTimeout = undefined;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const cleanup = (): void => {
|
|
538
|
+
clearPendingTimeout();
|
|
539
|
+
this.#pendingConnectSockets.delete(socket);
|
|
540
|
+
restoreRequestTimeoutHook();
|
|
541
|
+
restoreRequestDestroyHook();
|
|
542
|
+
socket.off(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
543
|
+
socket.off("error", onError);
|
|
544
|
+
socket.off("close", onClosed);
|
|
545
|
+
request?.off("abort", onRequestClosed);
|
|
546
|
+
request?.off("close", onRequestClosed);
|
|
547
|
+
request?.off("error", onRequestClosed);
|
|
548
|
+
request?.off("timeout", onRequestTimedOut);
|
|
549
|
+
};
|
|
550
|
+
const finish = (error: Error | null): void => {
|
|
551
|
+
if (settled) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
settled = true;
|
|
555
|
+
cleanup();
|
|
450
556
|
callback(error, socket);
|
|
451
557
|
};
|
|
558
|
+
const fail = (error: Error): void => {
|
|
559
|
+
socket.destroy();
|
|
560
|
+
finish(error);
|
|
561
|
+
};
|
|
562
|
+
const onError = (error: Error): void => {
|
|
563
|
+
finish(error);
|
|
564
|
+
};
|
|
565
|
+
const onClosed = (): void => {
|
|
566
|
+
finish(new ProxylineError("CONNECT_FAILED", "proxy socket closed before connection completed"));
|
|
567
|
+
};
|
|
452
568
|
const onConnected = (): void => {
|
|
453
|
-
|
|
454
|
-
|
|
569
|
+
finish(null);
|
|
570
|
+
};
|
|
571
|
+
const onRequestTimedOut = (): void => {
|
|
572
|
+
if (!settled) {
|
|
573
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
|
|
574
|
+
}
|
|
455
575
|
};
|
|
576
|
+
const onRequestClosed = (): void => {
|
|
577
|
+
if (!settled) {
|
|
578
|
+
fail(new ProxylineError("CONNECT_FAILED", "request closed before proxy connection completed"));
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
if (request !== undefined) {
|
|
583
|
+
originalRequestSetTimeout = request.setTimeout;
|
|
584
|
+
hookedRequestSetTimeout = function hookedSetTimeout(timeout, callback) {
|
|
585
|
+
const result = originalRequestSetTimeout?.call(this, timeout, callback) ?? this;
|
|
586
|
+
const timeoutMs = normalizedPositiveInteger(timeout);
|
|
587
|
+
if (timeoutMs !== undefined) {
|
|
588
|
+
startPendingTimeout(timeoutMs);
|
|
589
|
+
} else {
|
|
590
|
+
clearPendingTimeout();
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
};
|
|
594
|
+
request.setTimeout = hookedRequestSetTimeout;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Prefer explicit agent/request timeout. Default 30s only when both are omitted.
|
|
598
|
+
// Explicit values go through normalizedPositiveInteger first so fractional/invalid
|
|
599
|
+
// timeouts keep the historical no-pending-timer behavior (not Math.trunc to 1ms).
|
|
600
|
+
const requestTimeout = (request as { timeout?: unknown } | undefined)?.timeout;
|
|
601
|
+
const rawTimeout = options.timeout !== undefined ? options.timeout : requestTimeout;
|
|
602
|
+
if (rawTimeout === undefined) {
|
|
603
|
+
const connectTimeoutMs = resolveProxyConnectTimeoutMs(undefined);
|
|
604
|
+
if (connectTimeoutMs !== undefined) {
|
|
605
|
+
startPendingTimeout(connectTimeoutMs);
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
const timeoutMs = normalizedPositiveInteger(rawTimeout);
|
|
609
|
+
if (timeoutMs !== undefined) {
|
|
610
|
+
startPendingTimeout(timeoutMs);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
request?.once("timeout", onRequestTimedOut);
|
|
614
|
+
|
|
456
615
|
socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
457
616
|
socket.once("error", onError);
|
|
617
|
+
socket.once("close", onClosed);
|
|
618
|
+
// Node defers ClientRequest.destroy() until the async socket callback.
|
|
619
|
+
if (request !== undefined) {
|
|
620
|
+
originalRequestDestroy = request.destroy;
|
|
621
|
+
hookedRequestDestroy = function hookedDestroy(this: http.ClientRequest, error?: Error) {
|
|
622
|
+
const result = originalRequestDestroy?.call(this, error) ?? this;
|
|
623
|
+
onRequestClosed();
|
|
624
|
+
return result;
|
|
625
|
+
};
|
|
626
|
+
request.destroy = hookedRequestDestroy;
|
|
627
|
+
}
|
|
628
|
+
request?.once("abort", onRequestClosed);
|
|
629
|
+
request?.once("close", onRequestClosed);
|
|
630
|
+
request?.once("error", onRequestClosed);
|
|
631
|
+
if (request?.destroyed) {
|
|
632
|
+
onRequestClosed();
|
|
633
|
+
}
|
|
458
634
|
return undefined as unknown as net.Socket;
|
|
459
635
|
}
|
|
460
636
|
return socket;
|
|
461
637
|
}
|
|
638
|
+
|
|
639
|
+
public override destroy(): void {
|
|
640
|
+
for (const socket of this.#pendingConnectSockets) {
|
|
641
|
+
socket.destroy(new ProxylineError("CONNECT_FAILED", "proxy connection failed because agent was destroyed"));
|
|
642
|
+
}
|
|
643
|
+
this.#pendingConnectSockets.clear();
|
|
644
|
+
super.destroy();
|
|
645
|
+
}
|
|
462
646
|
}
|
|
463
647
|
|
|
464
|
-
class ProxylineConnectAgent extends
|
|
648
|
+
class ProxylineConnectAgent extends ProxylineRequestAgent {
|
|
465
649
|
public readonly options: NodeAgentOptions;
|
|
466
650
|
readonly #keepAlive: boolean;
|
|
467
651
|
readonly #pendingConnectSockets = new Set<net.Socket>();
|
|
468
|
-
readonly #pendingRequests = new WeakMap<NodeAgentRequestOptions, http.ClientRequest>();
|
|
469
|
-
readonly #pendingRequestQueue: http.ClientRequest[] = [];
|
|
470
652
|
readonly #proxy: URL;
|
|
471
653
|
readonly #proxyTls: ProxylineTlsOptions | undefined;
|
|
472
654
|
|
|
@@ -478,37 +660,11 @@ class ProxylineConnectAgent extends http.Agent {
|
|
|
478
660
|
this.#proxyTls = proxyTls;
|
|
479
661
|
}
|
|
480
662
|
|
|
481
|
-
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
482
|
-
this.#pendingRequests.set(options, req);
|
|
483
|
-
this.#pendingRequestQueue.push(req);
|
|
484
|
-
req.once("socket", () => this.#removePendingRequest(req));
|
|
485
|
-
req.once("close", () => this.#removePendingRequest(req));
|
|
486
|
-
try {
|
|
487
|
-
(http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
|
|
488
|
-
} catch (error) {
|
|
489
|
-
this.#pendingRequests.delete(options);
|
|
490
|
-
this.#removePendingRequest(req);
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
#removePendingRequest(req: http.ClientRequest): void {
|
|
496
|
-
const index = this.#pendingRequestQueue.indexOf(req);
|
|
497
|
-
if (index !== -1) {
|
|
498
|
-
this.#pendingRequestQueue.splice(index, 1);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
663
|
public override createConnection(
|
|
503
664
|
options: NodeAgentRequestOptions,
|
|
504
665
|
callback?: (error: Error | null, socket: net.Socket) => void,
|
|
505
666
|
): net.Socket {
|
|
506
|
-
const
|
|
507
|
-
const request = mappedRequest ?? this.#pendingRequestQueue.shift();
|
|
508
|
-
this.#pendingRequests.delete(options);
|
|
509
|
-
if (mappedRequest !== undefined) {
|
|
510
|
-
this.#removePendingRequest(mappedRequest);
|
|
511
|
-
}
|
|
667
|
+
const request = this.takePendingRequest(options);
|
|
512
668
|
if (callback === undefined) {
|
|
513
669
|
throw new ProxylineError("INVALID_CONNECT_CALLBACK", "CONNECT agents require an async socket callback.");
|
|
514
670
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -283,7 +283,11 @@ function normalizeProxyUrl(value: string | URL | undefined): URL | undefined {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
function emit(onEvent: ProxylineOptions["onEvent"], event: ProxylineEvent): void {
|
|
286
|
-
|
|
286
|
+
try {
|
|
287
|
+
onEvent?.(event);
|
|
288
|
+
} catch {
|
|
289
|
+
// Observer failures must not fail install or leave patched globals unstoppable.
|
|
290
|
+
}
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
function isProxyableUrlProtocol(protocol: string): boolean {
|
|
@@ -942,12 +946,6 @@ export function installProxyline(options: ProxylineOptions): ProxylineHandle {
|
|
|
942
946
|
},
|
|
943
947
|
)
|
|
944
948
|
: undefined;
|
|
945
|
-
emit(options.onEvent, {
|
|
946
|
-
type: "runtime.installed",
|
|
947
|
-
mode: options.mode,
|
|
948
|
-
active: hasActiveProxy,
|
|
949
|
-
...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
|
|
950
|
-
});
|
|
951
949
|
|
|
952
950
|
const handle: ProxylineHandle = {
|
|
953
951
|
mode: options.mode,
|
|
@@ -1012,6 +1010,12 @@ export function installProxyline(options: ProxylineOptions): ProxylineHandle {
|
|
|
1012
1010
|
};
|
|
1013
1011
|
|
|
1014
1012
|
activeHandle = hasActiveProxy ? handle : activeHandle;
|
|
1013
|
+
emit(options.onEvent, {
|
|
1014
|
+
type: "runtime.installed",
|
|
1015
|
+
mode: options.mode,
|
|
1016
|
+
active: hasActiveProxy,
|
|
1017
|
+
...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
|
|
1018
|
+
});
|
|
1015
1019
|
return handle;
|
|
1016
1020
|
}
|
|
1017
1021
|
|