@openclaw/proxyline 0.3.2 → 0.3.4

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4 - 2026-07-20
4
+
5
+ - Added `AbortSignal` cancellation to explicit CONNECT tunnels, including active socket cleanup.
6
+ - Fixed managed HTTPS proxy connections to IPv6 literal endpoints so equivalent compressed and expanded certificate addresses validate without sending IP-literal SNI.
7
+ - Updated Undici to 8.5.0 and raised the compatible peer floor to pick up current security fixes; refreshed pnpm, Node types, and transitive esbuild tooling.
8
+
9
+ ## 0.3.3 - 2026-05-18
10
+
11
+ - Fixed managed `globalThis.fetch` so later Undici global dispatcher replacement cannot bypass the active Proxyline dispatcher. Thanks @jesse-merhi.
12
+ - Updated package tooling and test dependencies to exact latest versions, including pnpm 11.1.2, TypeScript 6.0.3, and Undici 8.3.0; raised the Node.js floor to 22.19.0 to match Undici 8.
13
+
3
14
  ## 0.3.2 - 2026-05-17
4
15
 
5
16
  - Fixed managed Undici proxy dispatchers so HTTPS proxy endpoints addressed by IP do not send invalid IP-literal SNI.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 🌐 Proxyline
2
2
 
3
+ ![Proxyline banner](docs/assets/readme-banner.jpg)
4
+
3
5
  [![npm](https://img.shields.io/npm/v/%40openclaw%2Fproxyline.svg)](https://www.npmjs.com/package/@openclaw/proxyline)
4
6
  [![node](https://img.shields.io/node/v/%40openclaw%2Fproxyline.svg)](https://nodejs.org/)
5
7
  [![license](https://img.shields.io/npm/l/%40openclaw%2Fproxyline.svg)](./LICENSE)
@@ -31,7 +33,7 @@ pnpm add @openclaw/proxyline
31
33
  npm install @openclaw/proxyline
32
34
  ```
33
35
 
34
- Requires Node 20.18.1+ and a host `undici` dependency compatible with `>=7.25.0 <9`.
36
+ Requires Node 22.19.0+ and a host `undici` dependency compatible with `>=8.5.0 <9`.
35
37
 
36
38
  ## Quick start
37
39
 
@@ -76,12 +78,14 @@ const socket = new WebSocket("wss://events.example.com/", {
76
78
  ```ts
77
79
  import { openProxyConnectTunnel } from "@openclaw/proxyline";
78
80
 
81
+ const controller = new AbortController();
79
82
  const socket = await openProxyConnectTunnel({
80
83
  proxyUrl: "https://proxy.corp.example:8443",
81
84
  proxyTls: { caFile: "/etc/proxy-ca.pem" },
82
85
  targetHost: "api.example.com",
83
86
  targetPort: 443,
84
87
  timeoutMs: 2_000,
88
+ signal: controller.signal,
85
89
  });
86
90
  ```
87
91
 
@@ -103,7 +107,8 @@ It uses Proxyline's built-in HTTP/HTTPS Node agent, and `proxyTls` applies only
103
107
 
104
108
  - `http.request` / `http.get`: covered by global method patching and global agent replacement.
105
109
  - `https.request` / `https.get`: covered by global method patching and global agent replacement.
106
- - `fetch` / undici global dispatcher: covered by the `globalThis.fetch` patch and `setGlobalDispatcher`.
110
+ - `globalThis.fetch`: covered by the fetch patch, including explicit dispatcher options and later Undici global dispatcher replacement in managed mode.
111
+ - Undici global dispatcher: installed for Undici APIs that read the current process dispatcher.
107
112
  - WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
108
113
  - Caller-built `http.Agent` / `https.Agent`: overridden in managed and active ambient mode, with TLS options preserved.
109
114
  - Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
package/dist/connect.d.ts CHANGED
@@ -7,6 +7,7 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
7
7
  targetHost: string;
8
8
  targetPort: number;
9
9
  timeoutMs?: number;
10
+ signal?: AbortSignal;
10
11
  }>;
11
12
  type ProxySocket = net.Socket | tls.TLSSocket;
12
13
  export declare function formatConnectAuthority(targetHost: string, targetPort: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAkB,KAAK,mBAAmB,EAAqC,MAAM,aAAa,CAAC;AAE1G,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;IACvB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAMH,KAAK,WAAW,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC;AAsB9C,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAqBrF;AAkDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,WAAW,CAAC,CA8GtB"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAkB,KAAK,mBAAmB,EAAqC,MAAM,aAAa,CAAC;AAE1G,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;IACvB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC,CAAC;AAMH,KAAK,WAAW,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC;AAsB9C,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAqBrF;AAkDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,WAAW,CAAC,CAwHtB"}
package/dist/connect.js CHANGED
@@ -100,6 +100,7 @@ export async function openProxyConnectTunnel(options) {
100
100
  socket?.off("close", onClosed);
101
101
  socket?.off("connect", onConnected);
102
102
  socket?.off("secureConnect", onConnected);
103
+ options.signal?.removeEventListener("abort", onAbort);
103
104
  };
104
105
  const fail = (error) => {
105
106
  if (settled) {
@@ -162,7 +163,15 @@ export async function openProxyConnectTunnel(options) {
162
163
  const onClosed = () => {
163
164
  fail(new Error("proxy socket closed before CONNECT response"));
164
165
  };
166
+ const onAbort = () => {
167
+ fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
168
+ };
165
169
  try {
170
+ options.signal?.addEventListener("abort", onAbort, { once: true });
171
+ if (options.signal?.aborted) {
172
+ onAbort();
173
+ return;
174
+ }
166
175
  if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
167
176
  timeout = setTimeout(() => {
168
177
  fail(new Error(`proxy CONNECT timed out after ${Math.trunc(options.timeoutMs ?? 0)}ms`));
@@ -1 +1 @@
1
- {"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AAqwBpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,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;AAqzBpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
package/dist/runtime.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import http from "node:http";
2
2
  import https from "node:https";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { X509Certificate } from "node:crypto";
4
5
  import net from "node:net";
6
+ import tls from "node:tls";
5
7
  import { Agent as UndiciAgent, Dispatcher, FormData as UndiciFormData, Headers as UndiciHeaders, Pool as UndiciPool, Request as UndiciRequest, Response as UndiciResponse, errors as undiciErrors, fetch as undiciFetch, getGlobalDispatcher, ProxyAgent as UndiciProxyAgent, setGlobalDispatcher, } from "undici";
6
8
  import { createAmbientProxyResolver, EMPTY_PROXY_ENV, resolveAmbientProxyForUrl, readProxyEnv, } from "./env.js";
7
9
  import { bindNodeHttpMethod, createDirectNodeAgent, createNodeProxyAgent, } from "./node-http.js";
@@ -100,25 +102,32 @@ async function normalizeFetchInput(input, init, options) {
100
102
  preserveDispatcher: options.preserveDispatcher,
101
103
  });
102
104
  }
103
- function stripFetchDispatcher(init) {
104
- if (typeof init !== "object" || init === null) {
105
- return init;
106
- }
107
- const sanitized = Object.create(init);
105
+ function withManagedFetchDispatcher(init, dispatcher) {
106
+ if (init !== undefined &&
107
+ init !== null &&
108
+ typeof init !== "object" &&
109
+ typeof init !== "function") {
110
+ throw new TypeError(`Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`);
111
+ }
112
+ const sanitized = init === undefined || init === null ? {} : Object.create(init);
108
113
  Reflect.defineProperty(sanitized, "dispatcher", {
109
114
  configurable: true,
110
115
  enumerable: true,
111
- value: undefined,
116
+ value: dispatcher,
112
117
  writable: true,
113
118
  });
114
119
  return sanitized;
115
120
  }
116
121
  const proxylineFetch = async (input, init) => {
117
- const managedMode = activeRuntime?.mode === "managed";
122
+ const managedDispatcher = activeRuntime?.mode === "managed"
123
+ ? activeRuntime.installedDispatcher
124
+ : undefined;
118
125
  const normalizedInput = await normalizeFetchInput(input, init, {
119
- preserveDispatcher: !managedMode,
126
+ preserveDispatcher: managedDispatcher === undefined,
120
127
  });
121
- const normalizedInit = managedMode ? stripFetchDispatcher(init) : init;
128
+ const normalizedInit = managedDispatcher === undefined
129
+ ? init
130
+ : withManagedFetchDispatcher(init, managedDispatcher);
122
131
  const response = await Reflect.apply(undiciFetch, undefined, normalizedInit === undefined ? [normalizedInput] : [normalizedInput, normalizedInit]);
123
132
  if (!(response instanceof proxylineResponse)) {
124
133
  throw new TypeError("Proxyline fetch returned a non-Response value.");
@@ -294,6 +303,21 @@ function stripIpServernameFromConnect(connect) {
294
303
  }
295
304
  return (options, callback) => connect(stripIpServernameFromConnectOptions(options), callback);
296
305
  }
306
+ const checkProxyServerIdentity = (hostname, certificate) => {
307
+ const normalizedHostname = hostname.replace(/^\[|\]$/g, "");
308
+ if (net.isIP(normalizedHostname) === 6) {
309
+ try {
310
+ // Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent.
311
+ if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) {
312
+ return undefined;
313
+ }
314
+ }
315
+ catch {
316
+ // Let Node return its canonical certificate validation error below.
317
+ }
318
+ }
319
+ return tls.checkServerIdentity(normalizedHostname, certificate);
320
+ };
297
321
  function createProxyClientFactory() {
298
322
  return (origin, options) => {
299
323
  const clientOptions = isObjectRecord(options)
@@ -303,11 +327,18 @@ function createProxyClientFactory() {
303
327
  };
304
328
  }
305
329
  function createUndiciProxyAgent(proxyUrl, options) {
330
+ const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, "");
331
+ const proxyTls = options.proxyCa !== undefined || net.isIP(proxyHostname) === 6
332
+ ? {
333
+ ...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}),
334
+ ...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}),
335
+ }
336
+ : undefined;
306
337
  return new UndiciProxyAgent({
307
338
  ...resolveUndiciBaseOptions(options.undici),
308
339
  uri: proxyUrl,
309
340
  clientFactory: createProxyClientFactory(),
310
- ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
341
+ ...(proxyTls !== undefined ? { proxyTls } : {}),
311
342
  });
312
343
  }
313
344
  function createUndiciProxyDispatcher(options, dispatcherOptions) {
@@ -319,6 +350,18 @@ function createUndiciProxyDispatcher(options, dispatcherOptions) {
319
350
  }
320
351
  return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions);
321
352
  }
353
+ function reportClosedDispatchError(handler, error) {
354
+ const compatibleHandler = handler;
355
+ if (compatibleHandler.onResponseError !== undefined) {
356
+ compatibleHandler.onResponseError(null, error);
357
+ return false;
358
+ }
359
+ if (compatibleHandler.onError !== undefined) {
360
+ compatibleHandler.onError(error);
361
+ return false;
362
+ }
363
+ throw error;
364
+ }
322
365
  class ManagedUndiciDispatcher extends Dispatcher {
323
366
  [PROXYLINE_DISPATCHER_BRAND] = true;
324
367
  #directDispatcher;
@@ -334,11 +377,7 @@ class ManagedUndiciDispatcher extends Dispatcher {
334
377
  }
335
378
  dispatch(options, handler) {
336
379
  if (this.#closedError !== undefined) {
337
- if (handler.onError === undefined) {
338
- throw this.#closedError;
339
- }
340
- handler.onError(this.#closedError);
341
- return false;
380
+ return reportClosedDispatchError(handler, this.#closedError);
342
381
  }
343
382
  const url = resolveUndiciDispatchUrl(options);
344
383
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
@@ -404,11 +443,7 @@ class AmbientUndiciDispatcher extends Dispatcher {
404
443
  }
405
444
  dispatch(options, handler) {
406
445
  if (this.#closedError !== undefined) {
407
- if (handler.onError === undefined) {
408
- throw this.#closedError;
409
- }
410
- handler.onError(this.#closedError);
411
- return false;
446
+ return reportClosedDispatchError(handler, this.#closedError);
412
447
  }
413
448
  const url = resolveUndiciDispatchUrl(options);
414
449
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAKjD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW9F;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAO1D"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B,YAAmB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAI/C;CACF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW9F;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAO1D"}
package/docs/README.md CHANGED
@@ -25,7 +25,8 @@ Process-global proxy routing for Node.js. Proxyline patches the network surfaces
25
25
 
26
26
  - `http.request` / `http.get`: covered by global method patching and global agent replacement.
27
27
  - `https.request` / `https.get`: covered by global method patching and global agent replacement.
28
- - `fetch` / undici global dispatcher: covered by the `globalThis.fetch` patch and `setGlobalDispatcher`.
28
+ - `globalThis.fetch`: covered by the fetch patch, including explicit dispatcher options and later Undici global dispatcher replacement in managed mode.
29
+ - Undici global dispatcher: installed for Undici APIs that read the current process dispatcher.
29
30
  - WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
30
31
  - WebSocket clients without an `agent` option: partially covered when the upgrade path reuses patched `http.request`.
31
32
  - Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
@@ -257,6 +257,7 @@ type OpenProxyConnectTunnelOptions = Readonly<{
257
257
  targetHost: string;
258
258
  targetPort: number;
259
259
  timeoutMs?: number;
260
+ signal?: AbortSignal;
260
261
  }>;
261
262
  ```
262
263
 
@@ -264,6 +265,7 @@ type OpenProxyConnectTunnelOptions = Readonly<{
264
265
  - `proxyTls` — CA trust for HTTPS proxies. See [Proxy TLS](./proxy-tls.md).
265
266
  - `targetHost` / `targetPort` — what to ask the proxy to connect to.
266
267
  - `timeoutMs` — overall budget for the CONNECT handshake.
268
+ - `signal` — optional caller cancellation; aborting rejects the handshake and destroys its active proxy socket.
267
269
 
268
270
  ### `AmbientNodeProxyAgentOptions`
269
271
 
@@ -1,6 +1,6 @@
1
1
  # Getting Started
2
2
 
3
- Proxyline targets Node.js 20.18.1+. It is published as `@openclaw/proxyline` and ships ESM with TypeScript types.
3
+ Proxyline targets Node.js 22.19.0+. It is published as `@openclaw/proxyline` and ships ESM with TypeScript types.
4
4
 
5
5
  ## Install
6
6
 
package/docs/security.md CHANGED
@@ -30,7 +30,7 @@ Anything that does not flow through the patched APIs:
30
30
 
31
31
  - Direct `net.connect` or `tls.connect` calls. Code that opens raw sockets is not seen by Proxyline.
32
32
  - Native modules with private transport stacks (e.g. some database drivers, gRPC C bindings).
33
- - Libraries that import and call `undici.fetch` directly with their own explicit `Dispatcher`. Proxyline's managed `globalThis.fetch` strips explicit dispatchers, but it cannot rewrite every imported undici function reference.
33
+ - Libraries that import and call `undici.fetch` directly with their own explicit `Dispatcher`. Proxyline's managed `globalThis.fetch` pins its own dispatcher even when callers pass an explicit dispatcher or later replace the Undici global dispatcher, but it cannot rewrite every imported undici function reference.
34
34
  - DNS resolution itself. Proxyline tells the proxy a hostname; DNS-based exfiltration via the local resolver is out of scope.
35
35
  - Sockets opened **before** `installProxyline` ran. Existing keepalive connections continue to use whatever transport they were created with.
36
36
  - Module references captured before `installProxyline` ran. Anything that stored `http.request` in a local variable at import time keeps the un-patched reference.
@@ -69,7 +69,7 @@ This is deliberate: two competing proxy patches would race on `http.request` and
69
69
  | Threat | Mitigated | Notes |
70
70
  | --- | --- | --- |
71
71
  | Library passes a direct `http.Agent` per request | yes (managed) | Replaced before the request runs |
72
- | Library passes a direct `Dispatcher` to managed `globalThis.fetch` | yes | Explicit dispatchers are stripped |
72
+ | Library passes a direct `Dispatcher` to managed `globalThis.fetch` | yes | Proxyline pins its dispatcher |
73
73
  | Trusted local endpoint needs direct routing | yes, with `bypassPolicy` | Keep the callback narrow and auditable |
74
74
  | Library calls imported `undici.fetch` with a direct `Dispatcher` | no | Imported function references are outside the global fetch patch |
75
75
  | Library uses `net.connect` directly | no | Out of scope |
package/docs/surfaces.md CHANGED
@@ -50,7 +50,7 @@ import { fetch } from "undici";
50
50
  await fetch("https://api.example.com/health"); // routed through Proxyline
51
51
  ```
52
52
 
53
- In managed mode, Proxyline's patched `globalThis.fetch` ignores explicit `dispatcher` options so a per-call undici `Agent` cannot bypass the managed proxy. In ambient mode, and for callers using imported `undici.fetch` directly, an explicit undici `Agent` or `Dispatcher` still wins. Use `proxy.createUndiciDispatcher()` to get a dispatcher pre-wired to the same policy.
53
+ In managed mode, Proxyline's patched `globalThis.fetch` passes Proxyline's own dispatcher explicitly, so a per-call undici `Agent` or a later `setGlobalDispatcher()` call cannot bypass the managed proxy through that global fetch path. In ambient mode, and for callers using imported `undici.fetch` directly, an explicit undici `Agent` or `Dispatcher` still wins. Use `proxy.createUndiciDispatcher()` to get a dispatcher pre-wired to the same policy.
54
54
 
55
55
  Proxyline also replaces `globalThis.Request`, `Response`, `Headers`, and `FormData` with versions from its undici dependency so `globalThis.fetch` receives compatible objects on Node versions where the built-in fetch no longer shares the package dispatcher. Requests created before Proxyline installs are normalized through the standard public `Request` fields. Install Proxyline first if you need non-standard Request internals, such as a dispatcher embedded in a pre-install native `Request`, to be preserved.
56
56
 
@@ -83,6 +83,7 @@ const socket = await openProxyConnectTunnel({
83
83
  targetHost: "api.example.com",
84
84
  targetPort: 443,
85
85
  timeoutMs: 2_000,
86
+ signal: AbortSignal.timeout(2_000),
86
87
  });
87
88
  ```
88
89
 
@@ -93,6 +94,7 @@ Properties:
93
94
  - Userinfo in the `proxyUrl` becomes a `Proxy-Authorization: Basic ...` header.
94
95
  - A bounded `16 KiB` header buffer protects against malicious or runaway proxy responses.
95
96
  - `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry.
97
+ - `signal` aborts an in-progress handshake and destroys its active proxy socket.
96
98
  - Bytes the proxy sends after the response headers are re-injected with `socket.unshift()` so the caller sees the full target stream.
97
99
  - Non-2xx status lines, header overrun, premature close, and socket errors are all surfaced as `ProxylineError` with code `CONNECT_FAILED`.
98
100
 
package/docs/testing.md CHANGED
@@ -15,7 +15,7 @@ pnpm typecheck # type-only
15
15
 
16
16
  `pnpm test` builds `dist/`, then runs `node --test` via `tsx` against `test/index.test.ts` (unit), `test/e2e.test.ts` (integration), and `test/package.test.ts` (package entrypoint).
17
17
  `pnpm check` enforces native Node coverage for `src/**/*.ts` with thresholds of 85% lines, 80% branches, and 80% functions on Node versions that expose native threshold flags.
18
- CI runs that check on Ubuntu, macOS, and Windows across Node 20.18.1, 22, 24, and 26; Node 20.18.1 keeps the declared minimum covered with native coverage enabled and skips only the unavailable threshold/include flags.
18
+ CI runs that check on Ubuntu, macOS, and Windows across Node 22.19.0, 24, and 26; Node 22.19.0 keeps the declared minimum covered with native coverage enabled.
19
19
 
20
20
  ## What the lab covers
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/proxyline",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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@10.33.2",
20
+ "packageManager": "pnpm@11.9.0",
21
21
  "exports": {
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
@@ -53,20 +53,26 @@
53
53
  "prepare": "node scripts/prepack-build.mjs",
54
54
  "publish:dry-run": "pnpm publish --dry-run --access public",
55
55
  "test": "pnpm build && tsx --test test/index.test.ts test/e2e.test.ts test/package.test.ts test/scripts.test.ts && tsx --test test/package-artifact.test.ts",
56
- "typecheck": "pnpm build && tsc --noEmit"
56
+ "typecheck": "pnpm build && tsc --noEmit",
57
+ "check:changed": "pnpm run check",
58
+ "test:changed": "pnpm run test",
59
+ "crabbox:hydrate": "crabbox actions hydrate",
60
+ "crabbox:run": "crabbox run",
61
+ "crabbox:stop": "crabbox stop",
62
+ "crabbox:warmup": "crabbox warmup"
57
63
  },
58
64
  "devDependencies": {
59
- "@types/node": "^20.19.40",
60
- "@types/ws": "^8.18.1",
61
- "tsx": "^4.21.0",
62
- "typescript": "^5.9.3",
63
- "undici": "^7.25.0",
64
- "ws": "^8.20.0"
65
+ "@types/node": "26.1.1",
66
+ "@types/ws": "8.18.1",
67
+ "tsx": "4.23.0",
68
+ "typescript": "^7.0.2",
69
+ "undici": "8.6.0",
70
+ "ws": "8.21.0"
65
71
  },
66
72
  "engines": {
67
- "node": ">=20.18.1"
73
+ "node": ">=22.19.0"
68
74
  },
69
75
  "peerDependencies": {
70
- "undici": ">=7.25.0 <9"
76
+ "undici": ">=8.5.0 <9"
71
77
  }
72
78
  }
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
5
+ import { dirname, resolve } from "node:path";
4
6
 
5
7
  const require = createRequire(import.meta.url);
6
8
 
7
9
  function resolveTypeScriptCompiler() {
8
10
  try {
9
- return require.resolve("typescript/bin/tsc");
11
+ const packageJsonPath = require.resolve("typescript/package.json");
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
13
+ const compiler = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.tsc;
14
+ return typeof compiler === "string" ? resolve(dirname(packageJsonPath), compiler) : undefined;
10
15
  } catch {
11
16
  return undefined;
12
17
  }
package/src/connect.ts CHANGED
@@ -8,6 +8,7 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
8
8
  targetHost: string;
9
9
  targetPort: number;
10
10
  timeoutMs?: number;
11
+ signal?: AbortSignal;
11
12
  }>;
12
13
 
13
14
  const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
@@ -131,6 +132,7 @@ export async function openProxyConnectTunnel(
131
132
  socket?.off("close", onClosed);
132
133
  socket?.off("connect", onConnected);
133
134
  socket?.off("secureConnect", onConnected);
135
+ options.signal?.removeEventListener("abort", onAbort);
134
136
  };
135
137
 
136
138
  const fail = (error: unknown): void => {
@@ -203,7 +205,16 @@ export async function openProxyConnectTunnel(
203
205
  fail(new Error("proxy socket closed before CONNECT response"));
204
206
  };
205
207
 
208
+ const onAbort = (): void => {
209
+ fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
210
+ };
211
+
206
212
  try {
213
+ options.signal?.addEventListener("abort", onAbort, { once: true });
214
+ if (options.signal?.aborted) {
215
+ onAbort();
216
+ return;
217
+ }
207
218
  if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
208
219
  timeout = setTimeout(() => {
209
220
  fail(new Error(`proxy CONNECT timed out after ${Math.trunc(options.timeoutMs ?? 0)}ms`));
package/src/runtime.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import http from "node:http";
2
2
  import https from "node:https";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { X509Certificate } from "node:crypto";
4
5
  import net from "node:net";
6
+ import tls from "node:tls";
5
7
  import {
6
8
  Agent as UndiciAgent,
7
9
  Dispatcher,
@@ -74,6 +76,11 @@ type ProxylineDispatcher = Dispatcher & {
74
76
  [PROXYLINE_DISPATCHER_BRAND]?: true;
75
77
  };
76
78
 
79
+ type CompatibleDispatchHandler = Dispatcher.DispatchHandler & {
80
+ onError?: (error: Error) => void;
81
+ onResponseError?: (controller: Dispatcher.DispatchController | null, error: Error) => void;
82
+ };
83
+
77
84
  // Node's global fetch types come from bundled undici-types, while the runtime
78
85
  // implementation intentionally delegates to this package's undici dependency.
79
86
  const proxylineHeaders = UndiciHeaders as unknown as typeof globalThis.Headers;
@@ -216,28 +223,40 @@ async function normalizeFetchInput(
216
223
  });
217
224
  }
218
225
 
219
- function stripFetchDispatcher(
226
+ function withManagedFetchDispatcher(
220
227
  init: Parameters<typeof globalThis.fetch>[1],
228
+ dispatcher: Dispatcher,
221
229
  ): Parameters<typeof globalThis.fetch>[1] {
222
- if (typeof init !== "object" || init === null) {
223
- return init;
230
+ if (
231
+ init !== undefined &&
232
+ init !== null &&
233
+ typeof init !== "object" &&
234
+ typeof init !== "function"
235
+ ) {
236
+ throw new TypeError(
237
+ `Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`,
238
+ );
224
239
  }
225
- const sanitized = Object.create(init);
240
+ const sanitized = init === undefined || init === null ? {} : Object.create(init);
226
241
  Reflect.defineProperty(sanitized, "dispatcher", {
227
242
  configurable: true,
228
243
  enumerable: true,
229
- value: undefined,
244
+ value: dispatcher,
230
245
  writable: true,
231
246
  });
232
247
  return sanitized;
233
248
  }
234
249
 
235
250
  const proxylineFetch: typeof globalThis.fetch = async (input, init) => {
236
- const managedMode = activeRuntime?.mode === "managed";
251
+ const managedDispatcher = activeRuntime?.mode === "managed"
252
+ ? activeRuntime.installedDispatcher
253
+ : undefined;
237
254
  const normalizedInput = await normalizeFetchInput(input, init, {
238
- preserveDispatcher: !managedMode,
255
+ preserveDispatcher: managedDispatcher === undefined,
239
256
  });
240
- const normalizedInit = managedMode ? stripFetchDispatcher(init) : init;
257
+ const normalizedInit = managedDispatcher === undefined
258
+ ? init
259
+ : withManagedFetchDispatcher(init, managedDispatcher);
241
260
  const response: unknown = await Reflect.apply(
242
261
  undiciFetch,
243
262
  undefined,
@@ -466,6 +485,21 @@ function stripIpServernameFromConnect(connect: unknown): unknown {
466
485
  (connect as UnknownFunction)(stripIpServernameFromConnectOptions(options), callback);
467
486
  }
468
487
 
488
+ const checkProxyServerIdentity: typeof tls.checkServerIdentity = (hostname, certificate) => {
489
+ const normalizedHostname = hostname.replace(/^\[|\]$/g, "");
490
+ if (net.isIP(normalizedHostname) === 6) {
491
+ try {
492
+ // Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent.
493
+ if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) {
494
+ return undefined;
495
+ }
496
+ } catch {
497
+ // Let Node return its canonical certificate validation error below.
498
+ }
499
+ }
500
+ return tls.checkServerIdentity(normalizedHostname, certificate);
501
+ };
502
+
469
503
  function createProxyClientFactory(): UndiciProxyClientFactory {
470
504
  return (origin: URL, options: object): Dispatcher => {
471
505
  const clientOptions = isObjectRecord(options)
@@ -482,11 +516,19 @@ function createUndiciProxyAgent(
482
516
  proxyUrl: string,
483
517
  options: UndiciDispatcherOptions,
484
518
  ): UndiciProxyAgent {
519
+ const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, "");
520
+ const proxyTls =
521
+ options.proxyCa !== undefined || net.isIP(proxyHostname) === 6
522
+ ? {
523
+ ...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}),
524
+ ...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}),
525
+ }
526
+ : undefined;
485
527
  return new UndiciProxyAgent({
486
528
  ...resolveUndiciBaseOptions(options.undici),
487
529
  uri: proxyUrl,
488
530
  clientFactory: createProxyClientFactory(),
489
- ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
531
+ ...(proxyTls !== undefined ? { proxyTls } : {}),
490
532
  } as ConstructorParameters<typeof UndiciProxyAgent>[0]);
491
533
  }
492
534
 
@@ -505,6 +547,22 @@ function createUndiciProxyDispatcher(
505
547
  return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions);
506
548
  }
507
549
 
550
+ function reportClosedDispatchError(
551
+ handler: Dispatcher.DispatchHandler,
552
+ error: Error,
553
+ ): boolean {
554
+ const compatibleHandler = handler as CompatibleDispatchHandler;
555
+ if (compatibleHandler.onResponseError !== undefined) {
556
+ compatibleHandler.onResponseError(null, error);
557
+ return false;
558
+ }
559
+ if (compatibleHandler.onError !== undefined) {
560
+ compatibleHandler.onError(error);
561
+ return false;
562
+ }
563
+ throw error;
564
+ }
565
+
508
566
  class ManagedUndiciDispatcher extends Dispatcher {
509
567
  public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
510
568
  readonly #directDispatcher: UndiciAgent;
@@ -525,11 +583,7 @@ class ManagedUndiciDispatcher extends Dispatcher {
525
583
  handler: Dispatcher.DispatchHandler,
526
584
  ): boolean {
527
585
  if (this.#closedError !== undefined) {
528
- if (handler.onError === undefined) {
529
- throw this.#closedError;
530
- }
531
- handler.onError(this.#closedError);
532
- return false;
586
+ return reportClosedDispatchError(handler, this.#closedError);
533
587
  }
534
588
  const url = resolveUndiciDispatchUrl(options);
535
589
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
@@ -615,11 +669,7 @@ class AmbientUndiciDispatcher extends Dispatcher {
615
669
  handler: Dispatcher.DispatchHandler,
616
670
  ): boolean {
617
671
  if (this.#closedError !== undefined) {
618
- if (handler.onError === undefined) {
619
- throw this.#closedError;
620
- }
621
- handler.onError(this.#closedError);
622
- return false;
672
+ return reportClosedDispatchError(handler, this.#closedError);
623
673
  }
624
674
  const url = resolveUndiciDispatchUrl(options);
625
675
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);