@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 +5 -0
- package/README.md +3 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +30 -19
- package/docs/README.md +2 -1
- package/docs/getting-started.md +1 -1
- package/docs/security.md +2 -2
- package/docs/surfaces.md +1 -1
- package/docs/testing.md +1 -1
- package/package.json +10 -10
- package/src/runtime.ts +43 -18
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
|
|
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
|
|
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()`.
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
114
|
+
value: dispatcher,
|
|
112
115
|
writable: true,
|
|
113
116
|
});
|
|
114
117
|
return sanitized;
|
|
115
118
|
}
|
|
116
119
|
const proxylineFetch = async (input, init) => {
|
|
117
|
-
const
|
|
120
|
+
const managedDispatcher = activeRuntime?.mode === "managed"
|
|
121
|
+
? activeRuntime.installedDispatcher
|
|
122
|
+
: undefined;
|
|
118
123
|
const normalizedInput = await normalizeFetchInput(input, init, {
|
|
119
|
-
preserveDispatcher:
|
|
124
|
+
preserveDispatcher: managedDispatcher === undefined,
|
|
120
125
|
});
|
|
121
|
-
const normalizedInit =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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()`.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
Proxyline targets Node.js
|
|
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`
|
|
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 |
|
|
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`
|
|
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
|
|
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.
|
|
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@
|
|
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": "
|
|
60
|
-
"@types/ws": "
|
|
61
|
-
"tsx": "
|
|
62
|
-
"typescript": "
|
|
63
|
-
"undici": "
|
|
64
|
-
"ws": "
|
|
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": ">=
|
|
67
|
+
"node": ">=22.19.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"undici": ">=
|
|
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
|
|
224
|
+
function withManagedFetchDispatcher(
|
|
220
225
|
init: Parameters<typeof globalThis.fetch>[1],
|
|
226
|
+
dispatcher: Dispatcher,
|
|
221
227
|
): Parameters<typeof globalThis.fetch>[1] {
|
|
222
|
-
if (
|
|
223
|
-
|
|
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:
|
|
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
|
|
249
|
+
const managedDispatcher = activeRuntime?.mode === "managed"
|
|
250
|
+
? activeRuntime.installedDispatcher
|
|
251
|
+
: undefined;
|
|
237
252
|
const normalizedInput = await normalizeFetchInput(input, init, {
|
|
238
|
-
preserveDispatcher:
|
|
253
|
+
preserveDispatcher: managedDispatcher === undefined,
|
|
239
254
|
});
|
|
240
|
-
const normalizedInit =
|
|
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
|
-
|
|
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
|
-
|
|
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);
|