@openclaw/proxyline 0.1.0 → 0.2.0
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 +11 -1
- package/README.md +25 -9
- package/dist/connect.d.ts +1 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +30 -1
- package/dist/env.d.ts +12 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +202 -0
- package/dist/index.d.ts +4 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -521
- package/dist/node-http.d.ts +52 -0
- package/dist/node-http.d.ts.map +1 -0
- package/dist/node-http.js +702 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +512 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +3 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +17 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0 - 2026-05-14
|
|
6
|
+
|
|
7
|
+
- Added ambient Node proxy helper exports and replaced the `proxy-agent` dependency with Proxyline's scoped HTTP/HTTPS Node agent.
|
|
8
|
+
- Added a native Node coverage command and `pnpm check` coverage gates for source lines, branches, and functions.
|
|
9
|
+
- Expanded CI to run the coverage-gated check across Ubuntu, macOS, and Windows on Node 20.18.1, 22, 24, and 26.
|
|
10
|
+
- Hardened ambient proxy routing, CONNECT target validation, undici dispatcher cleanup, and generated package output after the runtime module split.
|
|
11
|
+
- Added managed-mode `bypassPolicy` support so trusted callers can intentionally route selected loopback/control-plane traffic directly while keeping the rest of managed traffic proxied.
|
|
12
|
+
- Stopped versioning generated `dist/` output in git; release and package dry-run flows now build `dist/` during `prepack` and still publish generated JavaScript, declarations, and declaration maps.
|
|
13
|
+
|
|
3
14
|
## 0.1.0 - 2026-05-11
|
|
4
15
|
|
|
5
16
|
- Initial public release of `@openclaw/proxyline` for process-global proxy routing in Node.js.
|
|
@@ -12,4 +23,3 @@
|
|
|
12
23
|
- Added credential-safe proxy authorization handling for proxy URLs with userinfo.
|
|
13
24
|
- Added in-process proxy lab coverage for HTTP, HTTPS, CONNECT, WebSocket, undici/fetch, proxy auth, loopback blocking, HTTPS proxies, TLS preservation, and IPv6 `NO_PROXY`.
|
|
14
25
|
- Added full documentation for getting started, modes, surfaces, API reference, environment variables, proxy TLS, observability, security, troubleshooting, and testing.
|
|
15
|
-
|
package/README.md
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
# Proxyline
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@openclaw/proxyline)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
Process-global proxy routing for Node.js. One install replaces `node:http`, `node:https`, the undici/fetch global dispatcher, and
|
|
7
|
+
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.
|
|
8
8
|
|
|
9
9
|
Proxyline exists to make proxy behavior **explicit, observable, and hard to bypass accidentally** — so that "all egress goes through this gateway" is something you encode in code rather than hope for from environment variables.
|
|
10
10
|
|
|
11
|
+
Proxyline's runtime assurances assume it is installed before application and plugin networking code is loaded. Code that captured networking functions before installation, uses raw sockets, or owns a private/native transport stack is outside the normal Proxyline model.
|
|
12
|
+
|
|
11
13
|
Website: [proxyline.dev](https://proxyline.dev)
|
|
12
14
|
|
|
13
15
|
## Highlights
|
|
14
16
|
|
|
15
17
|
- **Two modes.** `managed` forces traffic through a configured proxy and fails closed on bad config. `ambient` reads `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY` for tooling that needs environment compatibility.
|
|
16
18
|
- **Covers the surfaces that matter.** `http.request`, `http.get`, `https.request`, `https.get`, both global agents, the undici global dispatcher, and helpers for WebSocket agents and HTTP CONNECT sockets.
|
|
17
|
-
- **Replaces caller agents.** In managed mode, a per-request `http.Agent` passed by a library does not bypass the proxy. TLS options on the caller agent (`ca`, `cert`, `key`, `rejectUnauthorized`, …) are preserved so destination TLS still validates.
|
|
19
|
+
- **Replaces caller agents.** In managed mode and active ambient mode, a per-request `http.Agent` passed by a library does not bypass the proxy. TLS options on the caller agent (`ca`, `cert`, `key`, `rejectUnauthorized`, …) are preserved so destination TLS still validates.
|
|
18
20
|
- **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`.
|
|
19
21
|
- **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.
|
|
20
|
-
- **Restoreable.** `proxy.stop()` restores the captured Node HTTP(S) methods, global agents, and
|
|
22
|
+
- **Restoreable.** `proxy.stop()` restores the captured Node HTTP(S) methods, global agents, undici dispatcher, and fetch globals. The runtime is a process-wide singleton — a second install throws `RUNTIME_ALREADY_ACTIVE`.
|
|
21
23
|
|
|
22
24
|
## Install
|
|
23
25
|
|
|
@@ -27,7 +29,7 @@ pnpm add @openclaw/proxyline
|
|
|
27
29
|
npm install @openclaw/proxyline
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
Requires Node 20+.
|
|
32
|
+
Requires Node 20.18.1+.
|
|
31
33
|
|
|
32
34
|
## Quick start
|
|
33
35
|
|
|
@@ -81,15 +83,29 @@ const socket = await openProxyConnectTunnel({
|
|
|
81
83
|
});
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
### Conditional Node agent
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createAmbientNodeProxyAgent } from "@openclaw/proxyline";
|
|
90
|
+
|
|
91
|
+
const agent = createAmbientNodeProxyAgent({
|
|
92
|
+
protocol: "https",
|
|
93
|
+
proxyTls: { caFile: "/etc/proxy-ca.pem" },
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The helper returns `undefined` when ambient proxy env is not configured, so callers can pass an agent only when needed.
|
|
98
|
+
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.
|
|
99
|
+
|
|
84
100
|
## Feature matrix
|
|
85
101
|
|
|
86
102
|
| Surface | Covered | Notes |
|
|
87
103
|
| --- | --- | --- |
|
|
88
104
|
| `http.request` / `http.get` | yes | global method patch + global agent swap |
|
|
89
105
|
| `https.request` / `https.get` | yes | global method patch + global agent swap |
|
|
90
|
-
| `fetch` / undici global dispatcher | yes | `setGlobalDispatcher` |
|
|
106
|
+
| `fetch` / undici global dispatcher | yes | `globalThis.fetch` patch + `setGlobalDispatcher` |
|
|
91
107
|
| WebSocket clients accepting a Node `agent` | yes | `proxy.createWebSocketAgent()` |
|
|
92
|
-
| Caller-built `http.Agent` / `https.Agent` | overridden in managed mode | TLS options preserved |
|
|
108
|
+
| Caller-built `http.Agent` / `https.Agent` | overridden in managed and active ambient mode | TLS options preserved |
|
|
93
109
|
| Explicit HTTP CONNECT socket | yes | `openProxyConnectTunnel()` |
|
|
94
110
|
| Raw `net.connect` / `tls.connect` | no | out of scope, see [Security](./docs/security.md) |
|
|
95
111
|
| Native or private transport stacks | no | out of scope, see [Security](./docs/security.md) |
|
package/dist/connect.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
}>;
|
|
11
11
|
type ProxySocket = net.Socket | tls.TLSSocket;
|
|
12
|
+
export declare function formatConnectAuthority(targetHost: string, targetPort: number): string;
|
|
12
13
|
export declare function openProxyConnectTunnel(options: OpenProxyConnectTunnelOptions): Promise<ProxySocket>;
|
|
13
14
|
export {};
|
|
14
15
|
//# sourceMappingURL=connect.d.ts.map
|
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,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;
|
|
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"}
|
package/dist/connect.js
CHANGED
|
@@ -2,6 +2,8 @@ import net from "node:net";
|
|
|
2
2
|
import tls from "node:tls";
|
|
3
3
|
import { ProxylineError, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
4
4
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
5
|
+
const INVALID_CONNECT_AUTHORITY_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
6
|
+
const INVALID_CONNECT_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
5
7
|
function resolveProxyHost(proxy) {
|
|
6
8
|
return (proxy.hostname || proxy.host).replace(/^\[|\]$/g, "");
|
|
7
9
|
}
|
|
@@ -19,6 +21,27 @@ function resolveProxyAuthorization(proxy) {
|
|
|
19
21
|
const password = decodeURIComponent(proxy.password);
|
|
20
22
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
21
23
|
}
|
|
24
|
+
export function formatConnectAuthority(targetHost, targetPort) {
|
|
25
|
+
if (!Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65_535) {
|
|
26
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", `Invalid CONNECT target port: ${targetPort}`);
|
|
27
|
+
}
|
|
28
|
+
if (!targetHost || INVALID_CONNECT_AUTHORITY_PATTERN.test(targetHost)) {
|
|
29
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host is empty or unsafe.");
|
|
30
|
+
}
|
|
31
|
+
const unbracketedHost = targetHost.startsWith("[") && targetHost.endsWith("]")
|
|
32
|
+
? targetHost.slice(1, -1)
|
|
33
|
+
: targetHost;
|
|
34
|
+
if (net.isIP(unbracketedHost) === 6) {
|
|
35
|
+
return `[${unbracketedHost}]:${targetPort}`;
|
|
36
|
+
}
|
|
37
|
+
if (targetHost.includes("[") || targetHost.includes("]")) {
|
|
38
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host has invalid brackets.");
|
|
39
|
+
}
|
|
40
|
+
if (targetHost.includes(":") || INVALID_CONNECT_HOST_DELIMITER_PATTERN.test(targetHost)) {
|
|
41
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host is not a host name.");
|
|
42
|
+
}
|
|
43
|
+
return `${targetHost}:${targetPort}`;
|
|
44
|
+
}
|
|
22
45
|
function connectToProxy(proxy, proxyTls) {
|
|
23
46
|
const host = resolveProxyHost(proxy);
|
|
24
47
|
const connectOptions = {
|
|
@@ -40,6 +63,11 @@ function connectToProxy(proxy, proxyTls) {
|
|
|
40
63
|
}
|
|
41
64
|
throw new ProxylineError("UNSUPPORTED_PROXY_PROTOCOL", `CONNECT tunnels support http:// and https:// proxy endpoints: ${proxy.protocol}`);
|
|
42
65
|
}
|
|
66
|
+
function assertSupportedConnectProxyProtocol(proxy) {
|
|
67
|
+
if (proxy.protocol !== "http:" && proxy.protocol !== "https:") {
|
|
68
|
+
throw new ProxylineError("UNSUPPORTED_PROXY_PROTOCOL", `CONNECT tunnels support http:// and https:// proxy endpoints: ${proxy.protocol}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
43
71
|
function writeConnectRequest(socket, proxy, target) {
|
|
44
72
|
const headers = [`CONNECT ${target} HTTP/1.1`, `Host: ${target}`, "Proxy-Connection: Keep-Alive"];
|
|
45
73
|
const authorization = resolveProxyAuthorization(proxy);
|
|
@@ -54,7 +82,8 @@ function failConnect(proxy, error) {
|
|
|
54
82
|
}
|
|
55
83
|
export async function openProxyConnectTunnel(options) {
|
|
56
84
|
const proxy = options.proxyUrl instanceof URL ? new URL(options.proxyUrl.href) : new URL(options.proxyUrl);
|
|
57
|
-
|
|
85
|
+
assertSupportedConnectProxyProtocol(proxy);
|
|
86
|
+
const target = formatConnectAuthority(options.targetHost, options.targetPort);
|
|
58
87
|
return await new Promise((resolve, reject) => {
|
|
59
88
|
let settled = false;
|
|
60
89
|
let responseBuffer = Buffer.alloc(0);
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProxyResolver } from "./types.js";
|
|
2
|
+
export type ProxyEnvKey = "HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" | "NO_PROXY" | "http_proxy" | "https_proxy" | "all_proxy" | "no_proxy";
|
|
3
|
+
type LowerProxyEnvKey = "http_proxy" | "https_proxy" | "all_proxy" | "no_proxy";
|
|
4
|
+
export type ProxyEnvSnapshot = Readonly<Record<ProxyEnvKey, string | undefined>>;
|
|
5
|
+
export declare const EMPTY_PROXY_ENV: ProxyEnvSnapshot;
|
|
6
|
+
export declare function readProxyEnv(): ProxyEnvSnapshot;
|
|
7
|
+
export declare function readProxyEnvValue(env: ProxyEnvSnapshot, key: LowerProxyEnvKey): string | undefined;
|
|
8
|
+
export declare function proxyUrlWithDefaultScheme(proxyUrl: string): string;
|
|
9
|
+
export declare function resolveAmbientProxyForUrl(url: string | URL, env: ProxyEnvSnapshot): string | undefined;
|
|
10
|
+
export declare function createAmbientProxyResolver(env: ProxyEnvSnapshot): ProxyResolver;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,KAAK,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,eAAe,EAAE,gBAS7B,CAAC;AAEF,wBAAgB,YAAY,IAAI,gBAAgB,CAW/C;AAoBD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AAuHD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAmC/E"}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { formatUrl, redactProxyUrl } from "./shared.js";
|
|
2
|
+
export const EMPTY_PROXY_ENV = {
|
|
3
|
+
HTTP_PROXY: undefined,
|
|
4
|
+
HTTPS_PROXY: undefined,
|
|
5
|
+
ALL_PROXY: undefined,
|
|
6
|
+
NO_PROXY: undefined,
|
|
7
|
+
http_proxy: undefined,
|
|
8
|
+
https_proxy: undefined,
|
|
9
|
+
all_proxy: undefined,
|
|
10
|
+
no_proxy: undefined,
|
|
11
|
+
};
|
|
12
|
+
export function readProxyEnv() {
|
|
13
|
+
return {
|
|
14
|
+
HTTP_PROXY: process.env.HTTP_PROXY,
|
|
15
|
+
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
|
16
|
+
ALL_PROXY: process.env.ALL_PROXY,
|
|
17
|
+
NO_PROXY: process.env.NO_PROXY,
|
|
18
|
+
http_proxy: process.env.http_proxy,
|
|
19
|
+
https_proxy: process.env.https_proxy,
|
|
20
|
+
all_proxy: process.env.all_proxy,
|
|
21
|
+
no_proxy: process.env.no_proxy,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeEnvValue(value) {
|
|
25
|
+
const trimmed = value?.trim();
|
|
26
|
+
return trimmed ? trimmed : undefined;
|
|
27
|
+
}
|
|
28
|
+
function upperProxyEnvKey(key) {
|
|
29
|
+
switch (key) {
|
|
30
|
+
case "http_proxy":
|
|
31
|
+
return "HTTP_PROXY";
|
|
32
|
+
case "https_proxy":
|
|
33
|
+
return "HTTPS_PROXY";
|
|
34
|
+
case "all_proxy":
|
|
35
|
+
return "ALL_PROXY";
|
|
36
|
+
case "no_proxy":
|
|
37
|
+
return "NO_PROXY";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function readProxyEnvValue(env, key) {
|
|
41
|
+
return normalizeEnvValue(env[key]) ?? normalizeEnvValue(env[upperProxyEnvKey(key)]);
|
|
42
|
+
}
|
|
43
|
+
export function proxyUrlWithDefaultScheme(proxyUrl) {
|
|
44
|
+
return proxyUrl.includes("://") ? proxyUrl : `http://${proxyUrl}`;
|
|
45
|
+
}
|
|
46
|
+
function normalizeAmbientProxyUrl(proxyUrl) {
|
|
47
|
+
if (proxyUrl === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(proxyUrlWithDefaultScheme(proxyUrl));
|
|
52
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function defaultPort(protocol) {
|
|
59
|
+
if (protocol === "http:" || protocol === "ws:") {
|
|
60
|
+
return 80;
|
|
61
|
+
}
|
|
62
|
+
if (protocol === "https:" || protocol === "wss:") {
|
|
63
|
+
return 443;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function matchesNoProxy(url, env) {
|
|
68
|
+
const rawNoProxy = readProxyEnvValue(env, "no_proxy")?.toLowerCase();
|
|
69
|
+
if (!rawNoProxy) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (rawNoProxy === "*") {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const hostname = normalizeNoProxyHost(url.hostname);
|
|
76
|
+
const port = Number.parseInt(url.port, 10) || defaultPort(url.protocol);
|
|
77
|
+
for (const rawEntry of rawNoProxy.split(/[,\s]/)) {
|
|
78
|
+
if (!rawEntry) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const { host: parsedHost, port: entryPort } = parseNoProxyEntry(rawEntry);
|
|
82
|
+
let entryHost = normalizeNoProxyHost(parsedHost);
|
|
83
|
+
if (entryPort && entryPort !== port) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!/^[.*]/.test(entryHost)) {
|
|
87
|
+
if (hostname === entryHost) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (entryHost.startsWith("*")) {
|
|
93
|
+
entryHost = entryHost.slice(1);
|
|
94
|
+
}
|
|
95
|
+
if (entryHost.startsWith(".") &&
|
|
96
|
+
(hostname === entryHost.slice(1) || hostname.endsWith(entryHost))) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (!entryHost.startsWith(".") && hostname.endsWith(entryHost)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function normalizeNoProxyHost(hostname) {
|
|
106
|
+
const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
107
|
+
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
108
|
+
? normalized.slice(1, -1)
|
|
109
|
+
: normalized;
|
|
110
|
+
}
|
|
111
|
+
function parseNoProxyEntry(entry) {
|
|
112
|
+
const bracketedIpv6 = entry.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
113
|
+
if (bracketedIpv6) {
|
|
114
|
+
return {
|
|
115
|
+
host: bracketedIpv6[1] ?? "",
|
|
116
|
+
port: bracketedIpv6[2] ? Number.parseInt(bracketedIpv6[2], 10) : 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const lastColon = entry.lastIndexOf(":");
|
|
120
|
+
const hasSingleColon = lastColon !== -1 && entry.indexOf(":") === lastColon;
|
|
121
|
+
if (hasSingleColon) {
|
|
122
|
+
const possiblePort = entry.slice(lastColon + 1);
|
|
123
|
+
if (/^\d+$/.test(possiblePort)) {
|
|
124
|
+
return {
|
|
125
|
+
host: entry.slice(0, lastColon),
|
|
126
|
+
port: Number.parseInt(possiblePort, 10),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { host: entry, port: 0 };
|
|
131
|
+
}
|
|
132
|
+
function proxyEnvKeyForProtocol(protocol) {
|
|
133
|
+
if (protocol === "http:" || protocol === "ws:") {
|
|
134
|
+
return "http_proxy";
|
|
135
|
+
}
|
|
136
|
+
if (protocol === "https:" || protocol === "wss:") {
|
|
137
|
+
return "https_proxy";
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
function supportsProxyForUrlProtocol(protocol) {
|
|
142
|
+
return protocol === "http:" || protocol === "https:" || protocol === "ws:" || protocol === "wss:";
|
|
143
|
+
}
|
|
144
|
+
function resolveAmbientProxyEnvValue(env, key) {
|
|
145
|
+
return normalizeAmbientProxyUrl(readProxyEnvValue(env, key));
|
|
146
|
+
}
|
|
147
|
+
export function resolveAmbientProxyForUrl(url, env) {
|
|
148
|
+
let parsedUrl;
|
|
149
|
+
try {
|
|
150
|
+
parsedUrl = url instanceof URL ? new URL(url.href) : new URL(url);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const protocol = parsedUrl.protocol;
|
|
156
|
+
if (!supportsProxyForUrlProtocol(protocol)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (matchesNoProxy(parsedUrl, env)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const protocolProxyKey = proxyEnvKeyForProtocol(protocol);
|
|
163
|
+
if (protocolProxyKey === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return (resolveAmbientProxyEnvValue(env, protocolProxyKey) ??
|
|
167
|
+
resolveAmbientProxyEnvValue(env, "all_proxy"));
|
|
168
|
+
}
|
|
169
|
+
export function createAmbientProxyResolver(env) {
|
|
170
|
+
const configuredProxy = resolveAmbientProxyEnvValue(env, "http_proxy") ??
|
|
171
|
+
resolveAmbientProxyEnvValue(env, "https_proxy") ??
|
|
172
|
+
resolveAmbientProxyEnvValue(env, "all_proxy");
|
|
173
|
+
return {
|
|
174
|
+
active: configuredProxy !== undefined,
|
|
175
|
+
describeProxy: () => configuredProxy
|
|
176
|
+
? redactProxyUrl(proxyUrlWithDefaultScheme(configuredProxy))
|
|
177
|
+
: undefined,
|
|
178
|
+
explain: (url, surface) => {
|
|
179
|
+
const formattedUrl = formatUrl(url);
|
|
180
|
+
const parsedUrl = new URL(formattedUrl);
|
|
181
|
+
const proxyUrl = resolveAmbientProxyForUrl(formattedUrl, env);
|
|
182
|
+
if (proxyUrl !== undefined) {
|
|
183
|
+
return {
|
|
184
|
+
kind: "proxied",
|
|
185
|
+
reason: "ambient-proxy-active",
|
|
186
|
+
surface,
|
|
187
|
+
url: formattedUrl,
|
|
188
|
+
proxyUrl: redactProxyUrl(proxyUrl),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
kind: "direct",
|
|
193
|
+
reason: supportsProxyForUrlProtocol(parsedUrl.protocol) && matchesNoProxy(parsedUrl, env)
|
|
194
|
+
? "no-proxy-match"
|
|
195
|
+
: "ambient-proxy-not-configured",
|
|
196
|
+
surface,
|
|
197
|
+
url: formattedUrl,
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
getProxyForUrl: (url) => resolveAmbientProxyForUrl(url, env) ?? "",
|
|
201
|
+
};
|
|
202
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,52 +1,6 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import { type Dispatcher } from "undici";
|
|
3
|
-
import { type ProxylineTlsOptions } from "./shared.js";
|
|
4
|
-
export { ProxylineError, redactProxyUrl, resolveProxyTlsCa, type ProxylineTlsOptions, } from "./shared.js";
|
|
5
1
|
export { openProxyConnectTunnel, type OpenProxyConnectTunnelOptions } from "./connect.js";
|
|
6
|
-
export type
|
|
7
|
-
export
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
proxyUrl?: string | URL;
|
|
11
|
-
proxyTls?: ProxylineTlsOptions;
|
|
12
|
-
onEvent?: (event: ProxylineEvent) => void;
|
|
13
|
-
}>;
|
|
14
|
-
export type ProxylineDecision = Readonly<{
|
|
15
|
-
kind: "proxied" | "direct" | "blocked";
|
|
16
|
-
reason: string;
|
|
17
|
-
surface: ProxylineSurface;
|
|
18
|
-
url: string;
|
|
19
|
-
proxyUrl?: string;
|
|
20
|
-
}>;
|
|
21
|
-
export type ProxylineEvent = Readonly<{
|
|
22
|
-
type: "runtime.installed";
|
|
23
|
-
mode: ProxylineMode;
|
|
24
|
-
active: boolean;
|
|
25
|
-
proxyUrl?: string;
|
|
26
|
-
}> | Readonly<{
|
|
27
|
-
type: "runtime.stopped";
|
|
28
|
-
mode: ProxylineMode;
|
|
29
|
-
}> | Readonly<{
|
|
30
|
-
type: "decision";
|
|
31
|
-
decision: ProxylineDecision;
|
|
32
|
-
}> | Readonly<{
|
|
33
|
-
type: "warning";
|
|
34
|
-
code: string;
|
|
35
|
-
message: string;
|
|
36
|
-
}>;
|
|
37
|
-
export type ExplainOptions = Readonly<{
|
|
38
|
-
surface?: ProxylineSurface;
|
|
39
|
-
}>;
|
|
40
|
-
export type ProxylineHandle = Readonly<{
|
|
41
|
-
mode: ProxylineMode;
|
|
42
|
-
active: boolean;
|
|
43
|
-
proxyUrl?: string;
|
|
44
|
-
createNodeAgent: () => http.Agent;
|
|
45
|
-
createUndiciDispatcher: () => Dispatcher;
|
|
46
|
-
createWebSocketAgent: () => http.Agent;
|
|
47
|
-
explain: (url: string | URL, options?: ExplainOptions) => ProxylineDecision;
|
|
48
|
-
stop: () => void;
|
|
49
|
-
}>;
|
|
50
|
-
export declare function installProxyline(options: ProxylineOptions): ProxylineHandle;
|
|
51
|
-
export declare const installGlobalProxy: typeof installProxyline;
|
|
2
|
+
export { createAmbientNodeProxyAgent, hasAmbientNodeProxyConfigured, type AmbientNodeProxyAgentOptions, } from "./node-http.js";
|
|
3
|
+
export { installGlobalProxy, installProxyline } from "./runtime.js";
|
|
4
|
+
export { ProxylineError, redactProxyUrl, resolveProxyTlsCa, type ProxylineTlsOptions, } from "./shared.js";
|
|
5
|
+
export type { ExplainOptions, ProxylineBypassPolicy, ProxylineBypassRequest, ProxylineDecision, ProxylineEvent, ProxylineHandle, ProxylineMode, ProxylineOptions, ProxylineSurface, } from "./types.js";
|
|
52
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,EACL,2BAA2B,EAC3B,6BAA6B,EAC7B,KAAK,4BAA4B,GAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EACL,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|