@openclaw/proxyline 0.3.2 → 0.3.3

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 - 2026-05-18
4
+
5
+ - Fixed managed `globalThis.fetch` so later Undici global dispatcher replacement cannot bypass the active Proxyline dispatcher. Thanks @jesse-merhi.
6
+ - 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.
7
+
3
8
  ## 0.3.2 - 2026-05-17
4
9
 
5
10
  - Fixed managed Undici proxy dispatchers so HTTPS proxy endpoints addressed by IP do not send invalid IP-literal SNI.
package/README.md CHANGED
@@ -31,7 +31,7 @@ pnpm add @openclaw/proxyline
31
31
  npm install @openclaw/proxyline
32
32
  ```
33
33
 
34
- Requires Node 20.18.1+ and a host `undici` dependency compatible with `>=7.25.0 <9`.
34
+ Requires Node 22.19.0+ and a host `undici` dependency compatible with `>=8.3.0 <9`.
35
35
 
36
36
  ## Quick start
37
37
 
@@ -103,7 +103,8 @@ It uses Proxyline's built-in HTTP/HTTPS Node agent, and `proxyTls` applies only
103
103
 
104
104
  - `http.request` / `http.get`: covered by global method patching and global agent replacement.
105
105
  - `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`.
106
+ - `globalThis.fetch`: covered by the fetch patch, including explicit dispatcher options and later Undici global dispatcher replacement in managed mode.
107
+ - Undici global dispatcher: installed for Undici APIs that read the current process dispatcher.
107
108
  - WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
108
109
  - Caller-built `http.Agent` / `https.Agent`: overridden in managed and active ambient mode, with TLS options preserved.
109
110
  - Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
@@ -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":"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"}
package/dist/runtime.js CHANGED
@@ -100,25 +100,32 @@ async function normalizeFetchInput(input, init, options) {
100
100
  preserveDispatcher: options.preserveDispatcher,
101
101
  });
102
102
  }
103
- function stripFetchDispatcher(init) {
104
- if (typeof init !== "object" || init === null) {
105
- return init;
106
- }
107
- const sanitized = Object.create(init);
103
+ function withManagedFetchDispatcher(init, dispatcher) {
104
+ if (init !== undefined &&
105
+ init !== null &&
106
+ typeof init !== "object" &&
107
+ typeof init !== "function") {
108
+ throw new TypeError(`Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`);
109
+ }
110
+ const sanitized = init === undefined || init === null ? {} : Object.create(init);
108
111
  Reflect.defineProperty(sanitized, "dispatcher", {
109
112
  configurable: true,
110
113
  enumerable: true,
111
- value: undefined,
114
+ value: dispatcher,
112
115
  writable: true,
113
116
  });
114
117
  return sanitized;
115
118
  }
116
119
  const proxylineFetch = async (input, init) => {
117
- const managedMode = activeRuntime?.mode === "managed";
120
+ const managedDispatcher = activeRuntime?.mode === "managed"
121
+ ? activeRuntime.installedDispatcher
122
+ : undefined;
118
123
  const normalizedInput = await normalizeFetchInput(input, init, {
119
- preserveDispatcher: !managedMode,
124
+ preserveDispatcher: managedDispatcher === undefined,
120
125
  });
121
- const normalizedInit = managedMode ? stripFetchDispatcher(init) : init;
126
+ const normalizedInit = managedDispatcher === undefined
127
+ ? init
128
+ : withManagedFetchDispatcher(init, managedDispatcher);
122
129
  const response = await Reflect.apply(undiciFetch, undefined, normalizedInit === undefined ? [normalizedInput] : [normalizedInput, normalizedInit]);
123
130
  if (!(response instanceof proxylineResponse)) {
124
131
  throw new TypeError("Proxyline fetch returned a non-Response value.");
@@ -319,6 +326,18 @@ function createUndiciProxyDispatcher(options, dispatcherOptions) {
319
326
  }
320
327
  return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions);
321
328
  }
329
+ function reportClosedDispatchError(handler, error) {
330
+ const compatibleHandler = handler;
331
+ if (compatibleHandler.onResponseError !== undefined) {
332
+ compatibleHandler.onResponseError(null, error);
333
+ return false;
334
+ }
335
+ if (compatibleHandler.onError !== undefined) {
336
+ compatibleHandler.onError(error);
337
+ return false;
338
+ }
339
+ throw error;
340
+ }
322
341
  class ManagedUndiciDispatcher extends Dispatcher {
323
342
  [PROXYLINE_DISPATCHER_BRAND] = true;
324
343
  #directDispatcher;
@@ -334,11 +353,7 @@ class ManagedUndiciDispatcher extends Dispatcher {
334
353
  }
335
354
  dispatch(options, handler) {
336
355
  if (this.#closedError !== undefined) {
337
- if (handler.onError === undefined) {
338
- throw this.#closedError;
339
- }
340
- handler.onError(this.#closedError);
341
- return false;
356
+ return reportClosedDispatchError(handler, this.#closedError);
342
357
  }
343
358
  const url = resolveUndiciDispatchUrl(options);
344
359
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
@@ -404,11 +419,7 @@ class AmbientUndiciDispatcher extends Dispatcher {
404
419
  }
405
420
  dispatch(options, handler) {
406
421
  if (this.#closedError !== undefined) {
407
- if (handler.onError === undefined) {
408
- throw this.#closedError;
409
- }
410
- handler.onError(this.#closedError);
411
- return false;
422
+ return reportClosedDispatchError(handler, this.#closedError);
412
423
  }
413
424
  const url = resolveUndiciDispatchUrl(options);
414
425
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
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()`.
@@ -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
 
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.3",
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.1.2",
21
21
  "exports": {
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
@@ -56,17 +56,17 @@
56
56
  "typecheck": "pnpm build && tsc --noEmit"
57
57
  },
58
58
  "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"
59
+ "@types/node": "25.8.0",
60
+ "@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"
65
65
  },
66
66
  "engines": {
67
- "node": ">=20.18.1"
67
+ "node": ">=22.19.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "undici": ">=7.25.0 <9"
70
+ "undici": ">=8.3.0 <9"
71
71
  }
72
72
  }
package/src/runtime.ts CHANGED
@@ -74,6 +74,11 @@ type ProxylineDispatcher = Dispatcher & {
74
74
  [PROXYLINE_DISPATCHER_BRAND]?: true;
75
75
  };
76
76
 
77
+ type CompatibleDispatchHandler = Dispatcher.DispatchHandler & {
78
+ onError?: (error: Error) => void;
79
+ onResponseError?: (controller: Dispatcher.DispatchController | null, error: Error) => void;
80
+ };
81
+
77
82
  // Node's global fetch types come from bundled undici-types, while the runtime
78
83
  // implementation intentionally delegates to this package's undici dependency.
79
84
  const proxylineHeaders = UndiciHeaders as unknown as typeof globalThis.Headers;
@@ -216,28 +221,40 @@ async function normalizeFetchInput(
216
221
  });
217
222
  }
218
223
 
219
- function stripFetchDispatcher(
224
+ function withManagedFetchDispatcher(
220
225
  init: Parameters<typeof globalThis.fetch>[1],
226
+ dispatcher: Dispatcher,
221
227
  ): Parameters<typeof globalThis.fetch>[1] {
222
- if (typeof init !== "object" || init === null) {
223
- return init;
228
+ if (
229
+ init !== undefined &&
230
+ init !== null &&
231
+ typeof init !== "object" &&
232
+ typeof init !== "function"
233
+ ) {
234
+ throw new TypeError(
235
+ `Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`,
236
+ );
224
237
  }
225
- const sanitized = Object.create(init);
238
+ const sanitized = init === undefined || init === null ? {} : Object.create(init);
226
239
  Reflect.defineProperty(sanitized, "dispatcher", {
227
240
  configurable: true,
228
241
  enumerable: true,
229
- value: undefined,
242
+ value: dispatcher,
230
243
  writable: true,
231
244
  });
232
245
  return sanitized;
233
246
  }
234
247
 
235
248
  const proxylineFetch: typeof globalThis.fetch = async (input, init) => {
236
- const managedMode = activeRuntime?.mode === "managed";
249
+ const managedDispatcher = activeRuntime?.mode === "managed"
250
+ ? activeRuntime.installedDispatcher
251
+ : undefined;
237
252
  const normalizedInput = await normalizeFetchInput(input, init, {
238
- preserveDispatcher: !managedMode,
253
+ preserveDispatcher: managedDispatcher === undefined,
239
254
  });
240
- const normalizedInit = managedMode ? stripFetchDispatcher(init) : init;
255
+ const normalizedInit = managedDispatcher === undefined
256
+ ? init
257
+ : withManagedFetchDispatcher(init, managedDispatcher);
241
258
  const response: unknown = await Reflect.apply(
242
259
  undiciFetch,
243
260
  undefined,
@@ -505,6 +522,22 @@ function createUndiciProxyDispatcher(
505
522
  return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions);
506
523
  }
507
524
 
525
+ function reportClosedDispatchError(
526
+ handler: Dispatcher.DispatchHandler,
527
+ error: Error,
528
+ ): boolean {
529
+ const compatibleHandler = handler as CompatibleDispatchHandler;
530
+ if (compatibleHandler.onResponseError !== undefined) {
531
+ compatibleHandler.onResponseError(null, error);
532
+ return false;
533
+ }
534
+ if (compatibleHandler.onError !== undefined) {
535
+ compatibleHandler.onError(error);
536
+ return false;
537
+ }
538
+ throw error;
539
+ }
540
+
508
541
  class ManagedUndiciDispatcher extends Dispatcher {
509
542
  public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
510
543
  readonly #directDispatcher: UndiciAgent;
@@ -525,11 +558,7 @@ class ManagedUndiciDispatcher extends Dispatcher {
525
558
  handler: Dispatcher.DispatchHandler,
526
559
  ): boolean {
527
560
  if (this.#closedError !== undefined) {
528
- if (handler.onError === undefined) {
529
- throw this.#closedError;
530
- }
531
- handler.onError(this.#closedError);
532
- return false;
561
+ return reportClosedDispatchError(handler, this.#closedError);
533
562
  }
534
563
  const url = resolveUndiciDispatchUrl(options);
535
564
  const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
@@ -615,11 +644,7 @@ class AmbientUndiciDispatcher extends Dispatcher {
615
644
  handler: Dispatcher.DispatchHandler,
616
645
  ): boolean {
617
646
  if (this.#closedError !== undefined) {
618
- if (handler.onError === undefined) {
619
- throw this.#closedError;
620
- }
621
- handler.onError(this.#closedError);
622
- return false;
647
+ return reportClosedDispatchError(handler, this.#closedError);
623
648
  }
624
649
  const url = resolveUndiciDispatchUrl(options);
625
650
  const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);