@openclaw/proxyline 0.3.3 → 0.3.5

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.5 - 2026-08-01
4
+
5
+ - Fixed malformed percent-encoding in proxy credentials to fail through CONNECT promises and Node request errors with `INVALID_PROXY_USERINFO`. Thanks @SebTardif.
6
+ - Updated Undici, WebSocket and TypeScript tooling, pnpm, and pinned GitHub Actions to their latest stable releases.
7
+
8
+ ## 0.3.4 - 2026-07-20
9
+
10
+ - Added `AbortSignal` cancellation to explicit CONNECT tunnels, including active socket cleanup.
11
+ - Fixed managed HTTPS proxy connections to IPv6 literal endpoints so equivalent compressed and expanded certificate addresses validate without sending IP-literal SNI.
12
+ - 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.
13
+
3
14
  ## 0.3.3 - 2026-05-18
4
15
 
5
16
  - Fixed managed `globalThis.fetch` so later Undici global dispatcher replacement cannot bypass the active Proxyline dispatcher. Thanks @jesse-merhi.
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 22.19.0+ and a host `undici` dependency compatible with `>=8.3.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
 
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,EAAgD,KAAK,mBAAmB,EAAqC,MAAM,aAAa,CAAC;AAExI,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,CA4HtB"}
package/dist/connect.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import net from "node:net";
2
2
  import tls from "node:tls";
3
- import { ProxylineError, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
3
+ import { ProxylineError, decodeProxyUserinfoComponent, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
4
4
  const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
5
5
  const INVALID_CONNECT_AUTHORITY_PATTERN = /[\u0000-\u0020\u007f]/;
6
6
  const INVALID_CONNECT_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
@@ -17,8 +17,8 @@ function resolveProxyAuthorization(proxy) {
17
17
  if (!proxy.username && !proxy.password) {
18
18
  return undefined;
19
19
  }
20
- const username = decodeURIComponent(proxy.username);
21
- const password = decodeURIComponent(proxy.password);
20
+ const username = decodeProxyUserinfoComponent(proxy.username);
21
+ const password = decodeProxyUserinfoComponent(proxy.password);
22
22
  return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
23
23
  }
24
24
  export function formatConnectAuthority(targetHost, targetPort) {
@@ -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) {
@@ -108,7 +109,7 @@ export async function openProxyConnectTunnel(options) {
108
109
  settled = true;
109
110
  cleanup();
110
111
  socket?.destroy();
111
- reject(failConnect(proxy, error));
112
+ reject(error instanceof ProxylineError ? error : failConnect(proxy, error));
112
113
  };
113
114
  const succeed = (connectedSocket, tunneledBytes) => {
114
115
  if (settled) {
@@ -127,7 +128,12 @@ export async function openProxyConnectTunnel(options) {
127
128
  fail(new Error("proxy socket missing after connect"));
128
129
  return;
129
130
  }
130
- writeConnectRequest(socket, proxy, target);
131
+ try {
132
+ writeConnectRequest(socket, proxy, target);
133
+ }
134
+ catch (error) {
135
+ fail(error);
136
+ }
131
137
  };
132
138
  const onData = (chunk) => {
133
139
  responseBuffer = Buffer.concat([responseBuffer, chunk]);
@@ -162,7 +168,15 @@ export async function openProxyConnectTunnel(options) {
162
168
  const onClosed = () => {
163
169
  fail(new Error("proxy socket closed before CONNECT response"));
164
170
  };
171
+ const onAbort = () => {
172
+ fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
173
+ };
165
174
  try {
175
+ options.signal?.addEventListener("abort", onAbort, { once: true });
176
+ if (options.signal?.aborted) {
177
+ onAbort();
178
+ return;
179
+ }
166
180
  if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
167
181
  timeout = setTimeout(() => {
168
182
  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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"}
package/dist/node-http.js CHANGED
@@ -5,7 +5,7 @@ import tls from "node:tls";
5
5
  import { domainToASCII } from "node:url";
6
6
  import { readProxyEnv, resolveAmbientProxyForUrl, } from "./env.js";
7
7
  import { formatConnectAuthority } from "./connect.js";
8
- import { ProxylineError, resolveProxyTlsCa } from "./shared.js";
8
+ import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa } from "./shared.js";
9
9
  const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
10
10
  const INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
11
11
  const INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN = /[\u0000-\u0020\u007f]/;
@@ -133,8 +133,8 @@ function proxyAuthorization(proxy) {
133
133
  if (!proxy.username && !proxy.password) {
134
134
  return undefined;
135
135
  }
136
- const username = decodeURIComponent(proxy.username);
137
- const password = decodeURIComponent(proxy.password);
136
+ const username = decodeProxyUserinfoComponent(proxy.username);
137
+ const password = decodeProxyUserinfoComponent(proxy.password);
138
138
  return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
139
139
  }
140
140
  function proxyConnectOptions(proxy, proxyTls) {
@@ -445,26 +445,23 @@ class ProxylineConnectAgent extends http.Agent {
445
445
  finish(error, proxySocket);
446
446
  };
447
447
  const onConnected = () => {
448
- let target;
449
448
  try {
450
- target = connectTarget(options);
449
+ const { host, port } = connectTarget(options);
450
+ const authority = formatConnectAuthority(host, port);
451
+ const headers = [
452
+ `CONNECT ${authority} HTTP/1.1`,
453
+ `Host: ${authority}`,
454
+ `Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
455
+ ];
456
+ const authorization = proxyAuthorization(this.#proxy);
457
+ if (authorization !== undefined) {
458
+ headers.push(`Proxy-Authorization: ${authorization}`);
459
+ }
460
+ proxySocket.write([...headers, "", ""].join("\r\n"));
451
461
  }
452
462
  catch (error) {
453
463
  fail(error instanceof Error ? error : new Error(String(error)));
454
- return;
455
- }
456
- const { host, port } = target;
457
- const authority = formatConnectAuthority(host, port);
458
- const headers = [
459
- `CONNECT ${authority} HTTP/1.1`,
460
- `Host: ${authority}`,
461
- `Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
462
- ];
463
- const authorization = proxyAuthorization(this.#proxy);
464
- if (authorization !== undefined) {
465
- headers.push(`Proxy-Authorization: ${authorization}`);
466
464
  }
467
- proxySocket.write([...headers, "", ""].join("\r\n"));
468
465
  };
469
466
  const onData = (chunk) => {
470
467
  responseBuffer = Buffer.concat([responseBuffer, chunk]);
@@ -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;AA8xBpB,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;AAs0BpB,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";
@@ -301,6 +303,21 @@ function stripIpServernameFromConnect(connect) {
301
303
  }
302
304
  return (options, callback) => connect(stripIpServernameFromConnectOptions(options), callback);
303
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
+ };
304
321
  function createProxyClientFactory() {
305
322
  return (origin, options) => {
306
323
  const clientOptions = isObjectRecord(options)
@@ -310,13 +327,32 @@ function createProxyClientFactory() {
310
327
  };
311
328
  }
312
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;
337
+ const dispatcherFactory = createProxyClientFactory();
313
338
  return new UndiciProxyAgent({
314
339
  ...resolveUndiciBaseOptions(options.undici),
315
340
  uri: proxyUrl,
316
- clientFactory: createProxyClientFactory(),
317
- ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
341
+ clientFactory: dispatcherFactory,
342
+ factory: dispatcherFactory,
343
+ ...(proxyTls !== undefined ? { proxyTls } : {}),
318
344
  });
319
345
  }
346
+ function normalizeUndiciDispatchOptions(options) {
347
+ if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) {
348
+ return options;
349
+ }
350
+ const pathUrl = new URL(options.path);
351
+ return {
352
+ ...options,
353
+ path: `${pathUrl.pathname}${pathUrl.search}`,
354
+ };
355
+ }
320
356
  function createUndiciProxyDispatcher(options, dispatcherOptions) {
321
357
  if (options.mode === "ambient") {
322
358
  if (!options.active) {
@@ -355,10 +391,11 @@ class ManagedUndiciDispatcher extends Dispatcher {
355
391
  if (this.#closedError !== undefined) {
356
392
  return reportClosedDispatchError(handler, this.#closedError);
357
393
  }
358
- const url = resolveUndiciDispatchUrl(options);
394
+ const normalizedOptions = normalizeUndiciDispatchOptions(options);
395
+ const url = resolveUndiciDispatchUrl(normalizedOptions);
359
396
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
360
397
  const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
361
- return dispatcher.dispatch(options, handler);
398
+ return dispatcher.dispatch(normalizedOptions, handler);
362
399
  }
363
400
  close(callback) {
364
401
  const closing = this.#closeAll();
@@ -421,10 +458,11 @@ class AmbientUndiciDispatcher extends Dispatcher {
421
458
  if (this.#closedError !== undefined) {
422
459
  return reportClosedDispatchError(handler, this.#closedError);
423
460
  }
424
- const url = resolveUndiciDispatchUrl(options);
461
+ const normalizedOptions = normalizeUndiciDispatchOptions(options);
462
+ const url = resolveUndiciDispatchUrl(normalizedOptions);
425
463
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
426
464
  const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
427
- return dispatcher.dispatch(options, handler);
465
+ return dispatcher.dispatch(normalizedOptions, handler);
428
466
  }
429
467
  close(callback) {
430
468
  const closing = this.#closeAll();
package/dist/shared.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare class ProxylineError extends Error {
6
6
  readonly code: string;
7
7
  constructor(code: string, message: string);
8
8
  }
9
+ /** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
10
+ export declare function decodeProxyUserinfoComponent(value: string): string;
9
11
  export declare function resolveProxyTlsCa(options: ProxylineTlsOptions | undefined): string | undefined;
10
12
  export declare function formatUrl(value: string | URL): string;
11
13
  export declare function redactProxyUrl(value: string | URL): string;
@@ -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,2FAA2F;AAC3F,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASlE;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/dist/shared.js CHANGED
@@ -7,6 +7,15 @@ export class ProxylineError extends Error {
7
7
  this.code = code;
8
8
  }
9
9
  }
10
+ /** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
11
+ export function decodeProxyUserinfoComponent(value) {
12
+ try {
13
+ return decodeURIComponent(value);
14
+ }
15
+ catch {
16
+ throw new ProxylineError("INVALID_PROXY_USERINFO", "Proxy username or password contains invalid percent-encoding.");
17
+ }
18
+ }
10
19
  export function resolveProxyTlsCa(options) {
11
20
  if (!options) {
12
21
  return undefined;
@@ -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
 
package/docs/surfaces.md CHANGED
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/proxyline",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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.1.2",
20
+ "packageManager": "pnpm@11.18.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": "25.8.0",
65
+ "@types/node": "26.1.2",
60
66
  "@types/ws": "8.18.1",
61
- "tsx": "4.22.1",
62
- "typescript": "6.0.3",
63
- "undici": "8.3.0",
64
- "ws": "8.20.1"
67
+ "tsx": "4.23.1",
68
+ "typescript": "^7.0.2",
69
+ "undici": "8.9.0",
70
+ "ws": "8.21.1"
65
71
  },
66
72
  "engines": {
67
73
  "node": ">=22.19.0"
68
74
  },
69
75
  "peerDependencies": {
70
- "undici": ">=8.3.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
@@ -1,6 +1,6 @@
1
1
  import net from "node:net";
2
2
  import tls from "node:tls";
3
- import { ProxylineError, type ProxylineTlsOptions, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
3
+ import { ProxylineError, decodeProxyUserinfoComponent, type ProxylineTlsOptions, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
4
4
 
5
5
  export type OpenProxyConnectTunnelOptions = Readonly<{
6
6
  proxyUrl: string | URL;
@@ -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;
@@ -31,8 +32,8 @@ function resolveProxyAuthorization(proxy: URL): string | undefined {
31
32
  if (!proxy.username && !proxy.password) {
32
33
  return undefined;
33
34
  }
34
- const username = decodeURIComponent(proxy.username);
35
- const password = decodeURIComponent(proxy.password);
35
+ const username = decodeProxyUserinfoComponent(proxy.username);
36
+ const password = decodeProxyUserinfoComponent(proxy.password);
36
37
  return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
37
38
  }
38
39
 
@@ -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 => {
@@ -140,7 +142,7 @@ export async function openProxyConnectTunnel(
140
142
  settled = true;
141
143
  cleanup();
142
144
  socket?.destroy();
143
- reject(failConnect(proxy, error));
145
+ reject(error instanceof ProxylineError ? error : failConnect(proxy, error));
144
146
  };
145
147
 
146
148
  const succeed = (connectedSocket: ProxySocket, tunneledBytes: Buffer | undefined): void => {
@@ -161,7 +163,11 @@ export async function openProxyConnectTunnel(
161
163
  fail(new Error("proxy socket missing after connect"));
162
164
  return;
163
165
  }
164
- writeConnectRequest(socket, proxy, target);
166
+ try {
167
+ writeConnectRequest(socket, proxy, target);
168
+ } catch (error) {
169
+ fail(error);
170
+ }
165
171
  };
166
172
 
167
173
  const onData = (chunk: Buffer): void => {
@@ -203,7 +209,16 @@ export async function openProxyConnectTunnel(
203
209
  fail(new Error("proxy socket closed before CONNECT response"));
204
210
  };
205
211
 
212
+ const onAbort = (): void => {
213
+ fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
214
+ };
215
+
206
216
  try {
217
+ options.signal?.addEventListener("abort", onAbort, { once: true });
218
+ if (options.signal?.aborted) {
219
+ onAbort();
220
+ return;
221
+ }
207
222
  if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
208
223
  timeout = setTimeout(() => {
209
224
  fail(new Error(`proxy CONNECT timed out after ${Math.trunc(options.timeoutMs ?? 0)}ms`));
package/src/node-http.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type ProxyEnvSnapshot,
10
10
  } from "./env.js";
11
11
  import { formatConnectAuthority } from "./connect.js";
12
- import { ProxylineError, resolveProxyTlsCa, type ProxylineTlsOptions } from "./shared.js";
12
+ import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa, type ProxylineTlsOptions } from "./shared.js";
13
13
  import type { ProxylineSurface, ProxyResolver } from "./types.js";
14
14
 
15
15
  export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions & {
@@ -198,8 +198,8 @@ function proxyAuthorization(proxy: URL): string | undefined {
198
198
  if (!proxy.username && !proxy.password) {
199
199
  return undefined;
200
200
  }
201
- const username = decodeURIComponent(proxy.username);
202
- const password = decodeURIComponent(proxy.password);
201
+ const username = decodeProxyUserinfoComponent(proxy.username);
202
+ const password = decodeProxyUserinfoComponent(proxy.password);
203
203
  return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
204
204
  }
205
205
 
@@ -591,25 +591,22 @@ class ProxylineConnectAgent extends http.Agent {
591
591
  };
592
592
 
593
593
  const onConnected = (): void => {
594
- let target: { host: string; port: number };
595
594
  try {
596
- target = connectTarget(options);
595
+ const { host, port } = connectTarget(options);
596
+ const authority = formatConnectAuthority(host, port);
597
+ const headers = [
598
+ `CONNECT ${authority} HTTP/1.1`,
599
+ `Host: ${authority}`,
600
+ `Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
601
+ ];
602
+ const authorization = proxyAuthorization(this.#proxy);
603
+ if (authorization !== undefined) {
604
+ headers.push(`Proxy-Authorization: ${authorization}`);
605
+ }
606
+ proxySocket.write([...headers, "", ""].join("\r\n"));
597
607
  } catch (error) {
598
608
  fail(error instanceof Error ? error : new Error(String(error)));
599
- return;
600
- }
601
- const { host, port } = target;
602
- const authority = formatConnectAuthority(host, port);
603
- const headers = [
604
- `CONNECT ${authority} HTTP/1.1`,
605
- `Host: ${authority}`,
606
- `Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
607
- ];
608
- const authorization = proxyAuthorization(this.#proxy);
609
- if (authorization !== undefined) {
610
- headers.push(`Proxy-Authorization: ${authorization}`);
611
609
  }
612
- proxySocket.write([...headers, "", ""].join("\r\n"));
613
610
  };
614
611
 
615
612
  const onData = (chunk: Buffer): void => {
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,
@@ -483,6 +485,21 @@ function stripIpServernameFromConnect(connect: unknown): unknown {
483
485
  (connect as UnknownFunction)(stripIpServernameFromConnectOptions(options), callback);
484
486
  }
485
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
+
486
503
  function createProxyClientFactory(): UndiciProxyClientFactory {
487
504
  return (origin: URL, options: object): Dispatcher => {
488
505
  const clientOptions = isObjectRecord(options)
@@ -499,14 +516,37 @@ function createUndiciProxyAgent(
499
516
  proxyUrl: string,
500
517
  options: UndiciDispatcherOptions,
501
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;
527
+ const dispatcherFactory = createProxyClientFactory();
502
528
  return new UndiciProxyAgent({
503
529
  ...resolveUndiciBaseOptions(options.undici),
504
530
  uri: proxyUrl,
505
- clientFactory: createProxyClientFactory(),
506
- ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
531
+ clientFactory: dispatcherFactory,
532
+ factory: dispatcherFactory,
533
+ ...(proxyTls !== undefined ? { proxyTls } : {}),
507
534
  } as ConstructorParameters<typeof UndiciProxyAgent>[0]);
508
535
  }
509
536
 
537
+ function normalizeUndiciDispatchOptions(
538
+ options: Dispatcher.DispatchOptions,
539
+ ): Dispatcher.DispatchOptions {
540
+ if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) {
541
+ return options;
542
+ }
543
+ const pathUrl = new URL(options.path);
544
+ return {
545
+ ...options,
546
+ path: `${pathUrl.pathname}${pathUrl.search}`,
547
+ };
548
+ }
549
+
510
550
  function createUndiciProxyDispatcher(
511
551
  options:
512
552
  | { mode: "managed"; resolver: ProxyResolver }
@@ -560,10 +600,11 @@ class ManagedUndiciDispatcher extends Dispatcher {
560
600
  if (this.#closedError !== undefined) {
561
601
  return reportClosedDispatchError(handler, this.#closedError);
562
602
  }
563
- const url = resolveUndiciDispatchUrl(options);
603
+ const normalizedOptions = normalizeUndiciDispatchOptions(options);
604
+ const url = resolveUndiciDispatchUrl(normalizedOptions);
564
605
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
565
606
  const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
566
- return dispatcher.dispatch(options, handler);
607
+ return dispatcher.dispatch(normalizedOptions, handler);
567
608
  }
568
609
 
569
610
  public override close(callback: () => void): void;
@@ -646,10 +687,11 @@ class AmbientUndiciDispatcher extends Dispatcher {
646
687
  if (this.#closedError !== undefined) {
647
688
  return reportClosedDispatchError(handler, this.#closedError);
648
689
  }
649
- const url = resolveUndiciDispatchUrl(options);
690
+ const normalizedOptions = normalizeUndiciDispatchOptions(options);
691
+ const url = resolveUndiciDispatchUrl(normalizedOptions);
650
692
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
651
693
  const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
652
- return dispatcher.dispatch(options, handler);
694
+ return dispatcher.dispatch(normalizedOptions, handler);
653
695
  }
654
696
 
655
697
  public override close(callback: () => void): void;
package/src/shared.ts CHANGED
@@ -15,6 +15,18 @@ export class ProxylineError extends Error {
15
15
  }
16
16
  }
17
17
 
18
+ /** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
19
+ export function decodeProxyUserinfoComponent(value: string): string {
20
+ try {
21
+ return decodeURIComponent(value);
22
+ } catch {
23
+ throw new ProxylineError(
24
+ "INVALID_PROXY_USERINFO",
25
+ "Proxy username or password contains invalid percent-encoding.",
26
+ );
27
+ }
28
+ }
29
+
18
30
  export function resolveProxyTlsCa(options: ProxylineTlsOptions | undefined): string | undefined {
19
31
  if (!options) {
20
32
  return undefined;