@openclaw/proxyline 0.3.5 → 0.3.6
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 +59 -90
- package/dist/connect.d.ts +4 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +18 -3
- package/dist/node-http.d.ts.map +1 -1
- package/dist/node-http.js +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/surfaces.md +1 -1
- package/package.json +1 -1
- package/src/connect.ts +24 -3
- package/src/node-http.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.6 - 2026-08-02
|
|
4
|
+
|
|
5
|
+
- Fixed HTTP forwarding through HTTPS proxies to wait for the proxy TLS handshake before assigning the socket to Node's agent. Thanks @SebTardif.
|
|
6
|
+
- Added a 30-second default timeout to `openProxyConnectTunnel` when `timeoutMs` is omitted; pass `0` for unbounded waits. Thanks @SebTardif.
|
|
7
|
+
|
|
3
8
|
## 0.3.5 - 2026-08-01
|
|
4
9
|
|
|
5
10
|
- Fixed malformed percent-encoding in proxy credentials to fail through CONNECT promises and Node request errors with `INVALID_PROXY_USERINFO`. Thanks @SebTardif.
|
package/README.md
CHANGED
|
@@ -1,133 +1,97 @@
|
|
|
1
|
-
# 🌐
|
|
1
|
+
# Proxyline 🌐 — Keep every Node request in line.
|
|
2
2
|
|
|
3
|
-
](./LICENSE)
|
|
8
|
-
|
|
9
|
-
Process-global proxy routing for Node.js. One install replaces `node:http`, `node:https`, the undici/fetch global dispatcher, and provides WebSocket and explicit HTTP CONNECT helpers for the same policy.
|
|
3
|
+
[](https://github.com/openclaw/proxyline/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@openclaw/proxyline)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](./LICENSE)
|
|
10
7
|
|
|
11
|
-
Proxyline
|
|
8
|
+

|
|
12
9
|
|
|
13
|
-
Proxyline's
|
|
10
|
+
Proxyline installs one process-wide proxy policy across Node's built-in HTTP(S) clients, global fetch and Undici, compatible WebSocket clients, and explicit CONNECT tunnels. It is for Node applications that need one runtime to enforce proxy routing, explain its decisions, and restore the original networking globals.
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
Documentation is available at [proxyline.dev](https://proxyline.dev).
|
|
16
13
|
|
|
17
|
-
##
|
|
14
|
+
## Install
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- **Intentional bypasses only.** Managed mode can accept a `bypassPolicy` callback, process-wide `registerBypass()`, or async-scoped `withBypass()` calls for trusted loopback or control-plane traffic that must stay direct; every bypass is visible through `explain()`.
|
|
23
|
-
- **Embeddable runtime controls.** `ifActive` handles process singleton reuse/replacement, `undici` options tune dispatcher defaults, and `isProxylineDispatcher()` identifies Proxyline-owned dispatchers without constructor-name checks.
|
|
24
|
-
- **Scoped proxy CA trust.** `proxyTls.ca` / `proxyTls.caFile` trust a private CA for the proxy endpoint only — no `NODE_EXTRA_CA_CERTS` and no `NODE_TLS_REJECT_UNAUTHORIZED=0`.
|
|
25
|
-
- **Observable.** `proxy.explain(url)` returns a structured decision (`proxied` / `direct` with a `reason`), and an `onEvent` callback receives `runtime.installed`, `runtime.stopped`, and per-decision events. Proxy URLs are credential-redacted.
|
|
26
|
-
- **Restoreable.** `proxy.stop()` restores the captured Node HTTP(S) methods, global agents, undici dispatcher, and fetch globals. The runtime is a process-wide singleton; by default a second active install throws `RUNTIME_ALREADY_ACTIVE`, while `ifActive` can reuse or replace intentionally.
|
|
16
|
+
```sh
|
|
17
|
+
pnpm add @openclaw/proxyline undici@^8.5.0
|
|
18
|
+
```
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
Or with npm:
|
|
29
21
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
# or
|
|
33
|
-
npm install @openclaw/proxyline
|
|
22
|
+
```sh
|
|
23
|
+
npm install @openclaw/proxyline undici@^8.5.0
|
|
34
24
|
```
|
|
35
25
|
|
|
36
|
-
|
|
26
|
+
Proxyline requires Node.js 22.19.0 or newer and a host `undici` version in the `>=8.5.0 <9` range. The package is ESM-only and includes TypeScript declarations.
|
|
37
27
|
|
|
38
28
|
## Quick start
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
Save this as `proxy.mjs`:
|
|
41
31
|
|
|
42
|
-
```
|
|
32
|
+
```js
|
|
43
33
|
import { installGlobalProxy } from "@openclaw/proxyline";
|
|
44
34
|
|
|
45
35
|
const proxy = installGlobalProxy({
|
|
46
36
|
mode: "managed",
|
|
47
|
-
proxyUrl: "
|
|
48
|
-
proxyTls: { caFile: "/etc/proxy-ca.pem" },
|
|
49
|
-
onEvent: (event) => console.debug("[proxyline]", event),
|
|
37
|
+
proxyUrl: "http://127.0.0.1:3128",
|
|
50
38
|
});
|
|
39
|
+
console.log(proxy.explain("https://api.example.com/").reason);
|
|
40
|
+
proxy.stop();
|
|
41
|
+
```
|
|
51
42
|
|
|
52
|
-
|
|
43
|
+
```sh
|
|
44
|
+
node proxy.mjs
|
|
45
|
+
# managed-proxy-active
|
|
53
46
|
```
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
This asks Proxyline for a routing decision without connecting to the placeholder proxy. Install Proxyline before loading application or plugin code that may capture networking functions.
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
import { installGlobalProxy } from "@openclaw/proxyline";
|
|
50
|
+
## Choose a mode
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
if (!proxy.active) {
|
|
62
|
-
console.warn("no HTTP_PROXY/HTTPS_PROXY/ALL_PROXY set — egress will be direct");
|
|
63
|
-
}
|
|
64
|
-
```
|
|
52
|
+
Proxyline has two explicit routing modes:
|
|
65
53
|
|
|
66
|
-
|
|
54
|
+
| Mode | Configuration | Direct traffic |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `managed` | A required `proxyUrl` in code | Only through `bypassPolicy`, `registerBypass()`, or `withBypass()` |
|
|
57
|
+
| `ambient` | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` | Whenever the environment has no matching proxy |
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
import WebSocket from "ws";
|
|
59
|
+
Managed mode fails during setup when its proxy configuration is missing or unsupported. Ambient mode reads the environment once at installation and stays inactive when no supported HTTP or HTTPS proxy is configured. See [Modes](./docs/modes.md) and [Environment Variables](./docs/environment-variables.md) for the complete rules.
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
agent: proxy.createWebSocketAgent(),
|
|
73
|
-
});
|
|
74
|
-
```
|
|
61
|
+
## Covered traffic
|
|
75
62
|
|
|
76
|
-
|
|
63
|
+
| Surface | How Proxyline applies the policy |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `node:http` and `node:https` | Patches request methods and replaces global and caller-supplied agents |
|
|
66
|
+
| Global fetch and Undici | Installs a global dispatcher and a compatible fetch stack |
|
|
67
|
+
| WebSocket clients | Supplies `proxy.createWebSocketAgent()` for clients that accept a Node agent |
|
|
68
|
+
| Explicit tunnels | Supplies `openProxyConnectTunnel()` for callers that need the connected socket |
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
import { openProxyConnectTunnel } from "@openclaw/proxyline";
|
|
70
|
+
`proxy.createNodeAgent()` and `proxy.createUndiciDispatcher()` expose the same policy to libraries that accept an agent or dispatcher directly. The [surface guide](./docs/surfaces.md) describes ownership, TLS preservation, and cleanup for each API.
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
const socket = await openProxyConnectTunnel({
|
|
83
|
-
proxyUrl: "https://proxy.corp.example:8443",
|
|
84
|
-
proxyTls: { caFile: "/etc/proxy-ca.pem" },
|
|
85
|
-
targetHost: "api.example.com",
|
|
86
|
-
targetPort: 443,
|
|
87
|
-
timeoutMs: 2_000,
|
|
88
|
-
signal: controller.signal,
|
|
89
|
-
});
|
|
90
|
-
```
|
|
72
|
+
## Bypasses and proxy trust
|
|
91
73
|
|
|
92
|
-
|
|
74
|
+
Managed mode supports deliberate direct-routing exceptions. A `bypassPolicy` handles installation-time policy, `registerBypass()` registers an exact process-wide exception, and `withBypass()` limits an exception to one async context. Each decision remains visible through `explain()`.
|
|
93
75
|
|
|
94
|
-
|
|
95
|
-
import { createAmbientNodeProxyAgent } from "@openclaw/proxyline";
|
|
76
|
+
For an HTTPS proxy with a private CA, use `proxyTls.ca` or `proxyTls.caFile`. That trust applies only to the proxy connection; destination TLS validation remains separate. See [Proxy TLS](./docs/proxy-tls.md).
|
|
96
77
|
|
|
97
|
-
|
|
98
|
-
protocol: "https",
|
|
99
|
-
proxyTls: { caFile: "/etc/proxy-ca.pem" },
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The helper returns `undefined` when ambient proxy env is not configured, so callers can pass an agent only when needed.
|
|
104
|
-
It uses Proxyline's built-in HTTP/HTTPS Node agent, and `proxyTls` applies only to HTTPS proxy endpoints. SOCKS and PAC proxy schemes remain unsupported.
|
|
78
|
+
## Observability and lifecycle
|
|
105
79
|
|
|
106
|
-
|
|
80
|
+
`proxy.explain(url)` reports `proxied` or `direct`, the reason, the surface, and a credential-redacted proxy URL when one applies. The optional `onEvent` callback receives installation, shutdown, and decision events.
|
|
107
81
|
|
|
108
|
-
|
|
109
|
-
- `https.request` / `https.get`: covered by global method patching and global agent replacement.
|
|
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.
|
|
112
|
-
- WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
|
|
113
|
-
- Caller-built `http.Agent` / `https.Agent`: overridden in managed and active ambient mode, with TLS options preserved.
|
|
114
|
-
- Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
|
|
115
|
-
- Raw `net.connect` / `tls.connect`: out of scope; see [Security](./docs/security.md).
|
|
116
|
-
- Native or private transport stacks: out of scope; see [Security](./docs/security.md).
|
|
82
|
+
Only one Proxyline runtime is active in a process. A second installation fails by default; `ifActive` can reuse a compatible runtime or replace it intentionally. `proxy.stop()` restores the captured Node HTTP(S) methods, global agents, Undici dispatcher, and fetch globals. See [Observability](./docs/observability.md) and the [API Reference](./docs/api-reference.md).
|
|
117
83
|
|
|
118
|
-
##
|
|
84
|
+
## Security boundary
|
|
119
85
|
|
|
120
|
-
|
|
86
|
+
Proxyline is a Node-process runtime, not an operating-system sandbox. Raw `net` or `tls` sockets, native or private transport stacks, networking functions captured before installation, and DNS traffic are outside its boundary. Combine it with operating-system egress controls when code in the process is not trusted.
|
|
121
87
|
|
|
122
|
-
|
|
88
|
+
Read the [security model](./docs/security.md) before treating managed mode as an enforcement boundary.
|
|
123
89
|
|
|
124
90
|
## Documentation
|
|
125
91
|
|
|
126
|
-
Full docs live in [`docs/`](./docs/README.md):
|
|
127
|
-
|
|
128
92
|
- [Getting Started](./docs/getting-started.md)
|
|
129
|
-
- [Modes](./docs/modes.md)
|
|
130
|
-
- [Surfaces](./docs/surfaces.md)
|
|
93
|
+
- [Modes](./docs/modes.md)
|
|
94
|
+
- [Surfaces](./docs/surfaces.md)
|
|
131
95
|
- [API Reference](./docs/api-reference.md)
|
|
132
96
|
- [Environment Variables](./docs/environment-variables.md)
|
|
133
97
|
- [Proxy TLS](./docs/proxy-tls.md)
|
|
@@ -136,9 +100,14 @@ Full docs live in [`docs/`](./docs/README.md):
|
|
|
136
100
|
- [Troubleshooting](./docs/troubleshooting.md)
|
|
137
101
|
- [Testing](./docs/testing.md)
|
|
138
102
|
|
|
139
|
-
##
|
|
103
|
+
## Development
|
|
140
104
|
|
|
141
|
-
|
|
105
|
+
```sh
|
|
106
|
+
pnpm install --frozen-lockfile
|
|
107
|
+
pnpm check
|
|
108
|
+
pnpm test
|
|
109
|
+
pnpm docs:build
|
|
110
|
+
```
|
|
142
111
|
|
|
143
112
|
## License
|
|
144
113
|
|
package/dist/connect.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
6
6
|
proxyTls?: ProxylineTlsOptions;
|
|
7
7
|
targetHost: string;
|
|
8
8
|
targetPort: number;
|
|
9
|
+
/**
|
|
10
|
+
* Overall budget for the CONNECT handshake.
|
|
11
|
+
* Omit for the default (30s). Pass `0` (or a negative value) for no timeout.
|
|
12
|
+
*/
|
|
9
13
|
timeoutMs?: number;
|
|
10
14
|
signal?: AbortSignal;
|
|
11
15
|
}>;
|
package/dist/connect.d.ts.map
CHANGED
|
@@ -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,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;
|
|
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;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC,CAAC;AAsBH,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,CA6HtB"}
|
package/dist/connect.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import tls from "node:tls";
|
|
3
3
|
import { ProxylineError, decodeProxyUserinfoComponent, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
4
|
+
/** Default CONNECT handshake budget when `timeoutMs` is omitted. */
|
|
5
|
+
const DEFAULT_PROXY_CONNECT_TIMEOUT_MS = 30_000;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the CONNECT timeout to apply.
|
|
8
|
+
* - `undefined` → default 30s
|
|
9
|
+
* - `<= 0` → no timeout (unbounded)
|
|
10
|
+
* - positive → truncated milliseconds
|
|
11
|
+
*/
|
|
12
|
+
function resolveProxyConnectTimeoutMs(timeoutMs) {
|
|
13
|
+
if (timeoutMs === undefined) {
|
|
14
|
+
return DEFAULT_PROXY_CONNECT_TIMEOUT_MS;
|
|
15
|
+
}
|
|
16
|
+
return timeoutMs > 0 ? Math.trunc(timeoutMs) : undefined;
|
|
17
|
+
}
|
|
4
18
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
5
19
|
const INVALID_CONNECT_AUTHORITY_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
6
20
|
const INVALID_CONNECT_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
@@ -177,10 +191,11 @@ export async function openProxyConnectTunnel(options) {
|
|
|
177
191
|
onAbort();
|
|
178
192
|
return;
|
|
179
193
|
}
|
|
180
|
-
|
|
194
|
+
const connectTimeoutMs = resolveProxyConnectTimeoutMs(options.timeoutMs);
|
|
195
|
+
if (connectTimeoutMs !== undefined) {
|
|
181
196
|
timeout = setTimeout(() => {
|
|
182
|
-
fail(new Error(`proxy CONNECT timed out after ${
|
|
183
|
-
},
|
|
197
|
+
fail(new Error(`proxy CONNECT timed out after ${connectTimeoutMs}ms`));
|
|
198
|
+
}, connectTimeoutMs);
|
|
184
199
|
}
|
|
185
200
|
socket = connectToProxy(proxy, options.proxyTls);
|
|
186
201
|
socket.once(proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
package/dist/node-http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,MAAM,YAAY,CAAC;AAI/B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAmE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG;IAChF,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC;AAC1C,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC,KAAK,CAAC;AACxE,KAAK,gBAAgB,GAAG,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;AAC/D,KAAK,uBAAuB,GAAG,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,GAAG;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAYF,KAAK,qBAAqB,GAAG,gBAAgB,GAAG;IAC9C,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACnC,cAAc,EAAE,CACd,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,CAAC,EAAE,IAAI,CAAC,aAAa,KACzB,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CAChC,CAAC;AAOF,eAAO,MAAM,4BAA4B,YACvC,IAAI,EACJ,MAAM,EACN,SAAS,EACT,kBAAkB,EAClB,KAAK,EACL,SAAS,EACT,WAAW,EACX,kBAAkB,EAClB,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,CACV,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,OAAO,IAAI,CAAC,OAAO,CAAC;IACjC,OAAO,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC;IACzB,eAAe,EAAE,OAAO,IAAI,CAAC,WAAW,CAAC;IACzC,YAAY,EAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IACnC,QAAQ,EAAE,OAAO,KAAK,CAAC,GAAG,CAAC;IAC3B,gBAAgB,EAAE,OAAO,KAAK,CAAC,WAAW,CAAC;CAC5C,CAAC;AAiEF,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,cAAc,EAC/D,cAAc,EAAE,OAAO,EACvB,WAAW,EAAE,gBAAgB,GAC5B,OAAO,CAsCT;
|
|
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
|
@@ -314,6 +314,9 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
314
314
|
}
|
|
315
315
|
createConnection(_options, callback) {
|
|
316
316
|
const socket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
317
|
+
// When Node supplies an async callback, deliver the socket only through that
|
|
318
|
+
// path. Returning the same socket as well double-invokes Agent setup and can
|
|
319
|
+
// hand an unready TLS proxy socket to plain HTTP forward traffic.
|
|
317
320
|
if (callback !== undefined) {
|
|
318
321
|
const onError = (error) => {
|
|
319
322
|
callback(error, socket);
|
|
@@ -324,6 +327,7 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
324
327
|
};
|
|
325
328
|
socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
326
329
|
socket.once("error", onError);
|
|
330
|
+
return undefined;
|
|
327
331
|
}
|
|
328
332
|
return socket;
|
|
329
333
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -264,7 +264,7 @@ type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
264
264
|
- `proxyUrl` — `http://` or `https://`. Userinfo becomes a `Proxy-Authorization: Basic` header.
|
|
265
265
|
- `proxyTls` — CA trust for HTTPS proxies. See [Proxy TLS](./proxy-tls.md).
|
|
266
266
|
- `targetHost` / `targetPort` — what to ask the proxy to connect to.
|
|
267
|
-
- `timeoutMs` — overall budget for the CONNECT handshake.
|
|
267
|
+
- `timeoutMs` — overall budget for the CONNECT handshake. Defaults to `30000` when omitted. Pass `0` for no timeout.
|
|
268
268
|
- `signal` — optional caller cancellation; aborting rejects the handshake and destroys its active proxy socket.
|
|
269
269
|
|
|
270
270
|
### `AmbientNodeProxyAgentOptions`
|
package/docs/surfaces.md
CHANGED
|
@@ -93,7 +93,7 @@ Properties:
|
|
|
93
93
|
- HTTPS proxies use ALPN `http/1.1`. SNI is the proxy hostname unless that is an IP literal.
|
|
94
94
|
- Userinfo in the `proxyUrl` becomes a `Proxy-Authorization: Basic ...` header.
|
|
95
95
|
- A bounded `16 KiB` header buffer protects against malicious or runaway proxy responses.
|
|
96
|
-
- `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry.
|
|
96
|
+
- `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry. When omitted, the default is 30 seconds; pass `0` for no timeout.
|
|
97
97
|
- `signal` aborts an in-progress handshake and destroys its active proxy socket.
|
|
98
98
|
- Bytes the proxy sends after the response headers are re-injected with `socket.unshift()` so the caller sees the full target stream.
|
|
99
99
|
- Non-2xx status lines, header overrun, premature close, and socket errors are all surfaced as `ProxylineError` with code `CONNECT_FAILED`.
|
package/package.json
CHANGED
package/src/connect.ts
CHANGED
|
@@ -7,10 +7,30 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
7
7
|
proxyTls?: ProxylineTlsOptions;
|
|
8
8
|
targetHost: string;
|
|
9
9
|
targetPort: number;
|
|
10
|
+
/**
|
|
11
|
+
* Overall budget for the CONNECT handshake.
|
|
12
|
+
* Omit for the default (30s). Pass `0` (or a negative value) for no timeout.
|
|
13
|
+
*/
|
|
10
14
|
timeoutMs?: number;
|
|
11
15
|
signal?: AbortSignal;
|
|
12
16
|
}>;
|
|
13
17
|
|
|
18
|
+
/** Default CONNECT handshake budget when `timeoutMs` is omitted. */
|
|
19
|
+
const DEFAULT_PROXY_CONNECT_TIMEOUT_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the CONNECT timeout to apply.
|
|
23
|
+
* - `undefined` → default 30s
|
|
24
|
+
* - `<= 0` → no timeout (unbounded)
|
|
25
|
+
* - positive → truncated milliseconds
|
|
26
|
+
*/
|
|
27
|
+
function resolveProxyConnectTimeoutMs(timeoutMs: number | undefined): number | undefined {
|
|
28
|
+
if (timeoutMs === undefined) {
|
|
29
|
+
return DEFAULT_PROXY_CONNECT_TIMEOUT_MS;
|
|
30
|
+
}
|
|
31
|
+
return timeoutMs > 0 ? Math.trunc(timeoutMs) : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
15
35
|
const INVALID_CONNECT_AUTHORITY_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
16
36
|
const INVALID_CONNECT_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
@@ -219,10 +239,11 @@ export async function openProxyConnectTunnel(
|
|
|
219
239
|
onAbort();
|
|
220
240
|
return;
|
|
221
241
|
}
|
|
222
|
-
|
|
242
|
+
const connectTimeoutMs = resolveProxyConnectTimeoutMs(options.timeoutMs);
|
|
243
|
+
if (connectTimeoutMs !== undefined) {
|
|
223
244
|
timeout = setTimeout(() => {
|
|
224
|
-
fail(new Error(`proxy CONNECT timed out after ${
|
|
225
|
-
},
|
|
245
|
+
fail(new Error(`proxy CONNECT timed out after ${connectTimeoutMs}ms`));
|
|
246
|
+
}, connectTimeoutMs);
|
|
226
247
|
}
|
|
227
248
|
socket = connectToProxy(proxy, options.proxyTls);
|
|
228
249
|
socket.once(proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
package/src/node-http.ts
CHANGED
|
@@ -442,6 +442,9 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
442
442
|
callback?: (error: Error | null, socket: net.Socket) => void,
|
|
443
443
|
): net.Socket {
|
|
444
444
|
const socket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
445
|
+
// When Node supplies an async callback, deliver the socket only through that
|
|
446
|
+
// path. Returning the same socket as well double-invokes Agent setup and can
|
|
447
|
+
// hand an unready TLS proxy socket to plain HTTP forward traffic.
|
|
445
448
|
if (callback !== undefined) {
|
|
446
449
|
const onError = (error: Error): void => {
|
|
447
450
|
callback(error, socket);
|
|
@@ -452,6 +455,7 @@ class ProxylineHttpForwardAgent extends http.Agent {
|
|
|
452
455
|
};
|
|
453
456
|
socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
454
457
|
socket.once("error", onError);
|
|
458
|
+
return undefined as unknown as net.Socket;
|
|
455
459
|
}
|
|
456
460
|
return socket;
|
|
457
461
|
}
|