@openclaw/proxyline 0.3.0 → 0.3.2

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 - 2026-05-17
4
+
5
+ - Fixed managed Undici proxy dispatchers so HTTPS proxy endpoints addressed by IP do not send invalid IP-literal SNI.
6
+
7
+ ## 0.3.1 - 2026-05-16
8
+
9
+ - Fixed `withBypass()` to scope temporary bypasses to the calling async context instead of process-wide state.
10
+ - Fixed HTTPS SNI preservation when Node request options override a URL hostname.
11
+ - Hardened package and docs release output by preserving declaration-map sources, shipping product docs, rejecting duplicate docs pages, avoiding lockfile-bypassing prepack installs, and keeping package artifact checks portable on Windows.
12
+
3
13
  ## 0.3.0 - 2026-05-15
4
14
 
5
15
  - Added branded Proxyline dispatcher detection, reusable active-runtime installs, scoped dynamic bypass registration, first-class undici dispatcher tuning options, and a side-effect-light dispatcher detection subpath.
package/README.md CHANGED
@@ -17,7 +17,7 @@ Website: [proxyline.dev](https://proxyline.dev)
17
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.
18
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.
19
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.
20
- - **Intentional bypasses only.** Managed mode can accept a `bypassPolicy` callback or scoped `registerBypass()` / `withBypass()` calls for trusted loopback or control-plane traffic that must stay direct; every bypass is visible through `explain()`.
20
+ - **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()`.
21
21
  - **Embeddable runtime controls.** `ifActive` handles process singleton reuse/replacement, `undici` options tune dispatcher defaults, and `isProxylineDispatcher()` identifies Proxyline-owned dispatchers without constructor-name checks.
22
22
  - **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`.
23
23
  - **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.
@@ -99,18 +99,16 @@ const agent = createAmbientNodeProxyAgent({
99
99
  The helper returns `undefined` when ambient proxy env is not configured, so callers can pass an agent only when needed.
100
100
  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.
101
101
 
102
- ## Feature matrix
103
-
104
- | Surface | Covered | Notes |
105
- | --- | --- | --- |
106
- | `http.request` / `http.get` | yes | global method patch + global agent swap |
107
- | `https.request` / `https.get` | yes | global method patch + global agent swap |
108
- | `fetch` / undici global dispatcher | yes | `globalThis.fetch` patch + `setGlobalDispatcher` |
109
- | WebSocket clients accepting a Node `agent` | yes | `proxy.createWebSocketAgent()` |
110
- | Caller-built `http.Agent` / `https.Agent` | overridden in managed and active ambient mode | TLS options preserved |
111
- | Explicit HTTP CONNECT socket | yes | `openProxyConnectTunnel()` |
112
- | Raw `net.connect` / `tls.connect` | no | out of scope, see [Security](./docs/security.md) |
113
- | Native or private transport stacks | no | out of scope, see [Security](./docs/security.md) |
102
+ ## Product coverage
103
+
104
+ - `http.request` / `http.get`: covered by global method patching and global agent replacement.
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`.
107
+ - WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
108
+ - Caller-built `http.Agent` / `https.Agent`: overridden in managed and active ambient mode, with TLS options preserved.
109
+ - Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
110
+ - Raw `net.connect` / `tls.connect`: out of scope; see [Security](./docs/security.md).
111
+ - Native or private transport stacks: out of scope; see [Security](./docs/security.md).
114
112
 
115
113
  ## Why not just env vars?
116
114
 
@@ -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
@@ -53,15 +53,18 @@ function preserveCallerAgentOptions(options) {
53
53
  }
54
54
  }
55
55
  }
56
+ function unbracketHostname(hostname) {
57
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
58
+ }
56
59
  function inferDestinationHostname(url, options) {
57
- if (url !== undefined) {
58
- return url instanceof URL ? url.hostname : new URL(url).hostname;
59
- }
60
60
  if (typeof options.hostname === "string") {
61
- return options.hostname;
61
+ return unbracketHostname(options.hostname);
62
+ }
63
+ if (url !== undefined) {
64
+ return unbracketHostname(url instanceof URL ? url.hostname : new URL(url).hostname);
62
65
  }
63
66
  if (typeof options.host === "string") {
64
- return options.host.replace(/:\d*$/, "");
67
+ return unbracketHostname(splitHostPort(options.host).host);
65
68
  }
66
69
  return undefined;
67
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AA4sBpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAyI3E;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;AAqwBpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
package/dist/runtime.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import http from "node:http";
2
2
  import https from "node:https";
3
- import { Agent as UndiciAgent, Dispatcher, FormData as UndiciFormData, Headers as UndiciHeaders, Request as UndiciRequest, Response as UndiciResponse, errors as undiciErrors, fetch as undiciFetch, getGlobalDispatcher, ProxyAgent as UndiciProxyAgent, setGlobalDispatcher, } from "undici";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import net from "node:net";
5
+ 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";
4
6
  import { createAmbientProxyResolver, EMPTY_PROXY_ENV, resolveAmbientProxyForUrl, readProxyEnv, } from "./env.js";
5
7
  import { bindNodeHttpMethod, createDirectNodeAgent, createNodeProxyAgent, } from "./node-http.js";
6
8
  import { formatUrl, ProxylineError, redactProxyUrl, resolveProxyTlsCa, } from "./shared.js";
@@ -156,6 +158,12 @@ function bypassKey(url, surface) {
156
158
  }
157
159
  function createDynamicBypassRegistry() {
158
160
  const counts = new Map();
161
+ const scopedBypasses = new AsyncLocalStorage();
162
+ const hasScopedBypass = (url, surface) => {
163
+ const scoped = scopedBypasses.getStore();
164
+ return scoped !== undefined &&
165
+ (scoped.has(bypassKey(url, surface)) || scoped.has(bypassKey(url, undefined)));
166
+ };
159
167
  return {
160
168
  add: (registration) => {
161
169
  const key = bypassKey(registration.url, registration.surface);
@@ -175,8 +183,15 @@ function createDynamicBypassRegistry() {
175
183
  }
176
184
  };
177
185
  },
178
- has: (url, surface) => (counts.get(bypassKey(url, surface)) ?? 0) > 0 ||
186
+ has: (url, surface) => hasScopedBypass(url, surface) ||
187
+ (counts.get(bypassKey(url, surface)) ?? 0) > 0 ||
179
188
  (counts.get(bypassKey(url, undefined)) ?? 0) > 0,
189
+ runScoped: (registration, run) => {
190
+ const inherited = scopedBypasses.getStore();
191
+ const scoped = new Set(inherited);
192
+ scoped.add(bypassKey(registration.url, registration.surface));
193
+ return scopedBypasses.run(scoped, run);
194
+ },
180
195
  };
181
196
  }
182
197
  function proxyEnvSnapshotKey(env) {
@@ -258,10 +273,40 @@ function resolveUndiciBaseOptions(options) {
258
273
  function createUndiciAgent(options) {
259
274
  return new UndiciAgent(resolveUndiciBaseOptions(options));
260
275
  }
276
+ function isObjectRecord(value) {
277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
278
+ }
279
+ function stripIpServernameFromConnectOptions(options) {
280
+ if (!isObjectRecord(options) || typeof options.servername !== "string") {
281
+ return options;
282
+ }
283
+ const servername = options.servername.replace(/^\[|\]$/g, "");
284
+ if (net.isIP(servername) === 0) {
285
+ return options;
286
+ }
287
+ const next = { ...options };
288
+ delete next.servername;
289
+ return next;
290
+ }
291
+ function stripIpServernameFromConnect(connect) {
292
+ if (typeof connect !== "function") {
293
+ return connect;
294
+ }
295
+ return (options, callback) => connect(stripIpServernameFromConnectOptions(options), callback);
296
+ }
297
+ function createProxyClientFactory() {
298
+ return (origin, options) => {
299
+ const clientOptions = isObjectRecord(options)
300
+ ? { ...options, connect: stripIpServernameFromConnect(options.connect) }
301
+ : options;
302
+ return new UndiciPool(origin, clientOptions);
303
+ };
304
+ }
261
305
  function createUndiciProxyAgent(proxyUrl, options) {
262
306
  return new UndiciProxyAgent({
263
307
  ...resolveUndiciBaseOptions(options.undici),
264
308
  uri: proxyUrl,
309
+ clientFactory: createProxyClientFactory(),
265
310
  ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
266
311
  });
267
312
  }
@@ -624,28 +669,13 @@ export function installProxyline(options) {
624
669
  emit(options.onEvent, { type: "runtime.stopped", mode: options.mode });
625
670
  },
626
671
  withBypass: (registration, run) => {
627
- const unregister = handle.registerBypass(registration);
628
- try {
629
- const result = run();
630
- if (isPromiseLike(result)) {
631
- void Promise.resolve(result).then(unregister, unregister);
632
- return result;
633
- }
634
- unregister();
635
- return result;
636
- }
637
- catch (error) {
638
- unregister();
639
- throw error;
672
+ if (stopped || proxyUrl === undefined) {
673
+ return run();
640
674
  }
675
+ return dynamicBypasses.runScoped(registration, run);
641
676
  },
642
677
  };
643
678
  activeHandle = hasActiveProxy ? handle : activeHandle;
644
679
  return handle;
645
680
  }
646
681
  export const installGlobalProxy = installProxyline;
647
- function isPromiseLike(value) {
648
- return typeof value === "object" &&
649
- value !== null &&
650
- typeof value.then === "function";
651
- }
package/docs/CNAME ADDED
@@ -0,0 +1 @@
1
+ proxyline.dev
package/docs/README.md ADDED
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Overview
3
+ permalink: /
4
+ description: "Process-global proxy routing for Node.js. One install routes node:http, node:https, undici/fetch, plus WebSocket and HTTP CONNECT helpers through a single explicit policy."
5
+ ---
6
+
7
+ # Proxyline Documentation
8
+
9
+ Process-global proxy routing for Node.js. Proxyline patches the network surfaces a Node process can reach without owning a private transport stack, so a single policy applies to `node:http`, `node:https`, undici/fetch, WebSocket clients that accept agents, and explicit HTTP CONNECT helpers.
10
+
11
+ ## Contents
12
+
13
+ - [Getting Started](./getting-started.md) — install, first proxy, shutdown.
14
+ - [Modes](./modes.md) — `managed` vs `ambient` safety postures.
15
+ - [Surfaces](./surfaces.md) — which network APIs Proxyline covers and how.
16
+ - [API Reference](./api-reference.md) — every exported type, function, and field.
17
+ - [Environment Variables](./environment-variables.md) — how `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` are interpreted.
18
+ - [Proxy TLS](./proxy-tls.md) — scoping CA trust to the proxy endpoint.
19
+ - [Observability](./observability.md) — events, `explain()`, credential redaction.
20
+ - [Security](./security.md) — threat model, limits, what Proxyline does **not** do.
21
+ - [Troubleshooting](./troubleshooting.md) — common failure modes and fixes.
22
+ - [Testing](./testing.md) — the in-process proxy lab.
23
+
24
+ ## Product coverage
25
+
26
+ - `http.request` / `http.get`: covered by global method patching and global agent replacement.
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`.
29
+ - WebSocket clients accepting a Node `agent`: covered with `proxy.createWebSocketAgent()`.
30
+ - WebSocket clients without an `agent` option: partially covered when the upgrade path reuses patched `http.request`.
31
+ - Explicit HTTP CONNECT sockets: covered with `openProxyConnectTunnel()`.
32
+ - Caller-built `http.Agent` / `https.Agent`: overridden per request in managed and active ambient mode.
33
+ - Raw `net.connect` / `tls.connect`: out of scope; see [Security](./security.md).
34
+ - Native or third-party transport stacks: out of scope; see [Security](./security.md).
@@ -0,0 +1,280 @@
1
+ # API Reference
2
+
3
+ Every public export, with the exact shape from `src/index.ts`, `src/connect.ts`, and `src/node-http.ts`.
4
+
5
+ ## Functions
6
+
7
+ ### `installProxyline(options): ProxylineHandle`
8
+
9
+ Aliased as `installGlobalProxy`. Installs the runtime and returns a handle.
10
+
11
+ - Throws `ProxylineError` with code `MANAGED_PROXY_URL_REQUIRED` if `mode: "managed"` is used without a `proxyUrl`.
12
+ - Throws `ProxylineError` with code `UNSUPPORTED_PROXY_PROTOCOL` if managed-mode `proxyUrl` is not `http://` or `https://`.
13
+ - Throws `ProxylineError` with code `RUNTIME_ALREADY_ACTIVE` if another Proxyline runtime is already installed and `ifActive` is omitted or cannot safely reuse the active runtime.
14
+
15
+ In managed mode (and active ambient mode), `installProxyline`:
16
+
17
+ - Captures originals for `http.request`, `http.get`, `http.globalAgent`, `https.request`, `https.get`, `https.globalAgent`.
18
+ - Captures the current undici global dispatcher and fetch globals.
19
+ - Installs patched `http.request`/`get`, `https.request`/`get`.
20
+ - Replaces `http.globalAgent` and `https.globalAgent` with Proxyline's HTTP/HTTPS Node agent.
21
+ - Calls `undici.setGlobalDispatcher` with a `ProxyAgent` (managed) or Proxyline's ambient dispatcher (ambient), and patches `globalThis.fetch` plus `Request`, `Response`, `Headers`, and `FormData` to use that dispatcher-compatible fetch stack.
22
+ - Emits `runtime.installed`.
23
+
24
+ In inactive ambient mode (no supported proxy env variables), no patches are installed; the handle returns a passive observer with `active: false`.
25
+
26
+ ### `openProxyConnectTunnel(options): Promise<net.Socket | tls.TLSSocket>`
27
+
28
+ Opens a one-shot HTTP CONNECT tunnel through a proxy. See [Surfaces — HTTP CONNECT tunnel](./surfaces.md#http-connect-tunnel).
29
+
30
+ ### `hasAmbientNodeProxyConfigured(options?): boolean`
31
+
32
+ Returns `true` when the ambient proxy environment would proxy a probe URL for the requested protocol. Defaults to `protocol: "https"`.
33
+
34
+ ```ts
35
+ hasAmbientNodeProxyConfigured({ protocol: "https" });
36
+ ```
37
+
38
+ ### `createAmbientNodeProxyAgent(options?): http.Agent | undefined`
39
+
40
+ Returns a Proxyline-backed Node agent when ambient env proxy settings apply, or `undefined` when no proxy is configured for the requested protocol. This is for libraries that accept a Node `agent` option but should stay direct when the operator has no proxy env configured.
41
+
42
+ ```ts
43
+ const agent = createAmbientNodeProxyAgent({
44
+ protocol: "https",
45
+ proxyTls: { caFile: "/etc/proxy-ca.pem" },
46
+ });
47
+ ```
48
+
49
+ ### `redactProxyUrl(value: string | URL): string`
50
+
51
+ Strips userinfo, search, and fragment from a URL. Used internally to keep events and decisions free of credentials. Safe to use on log lines you build yourself.
52
+
53
+ ```ts
54
+ redactProxyUrl("https://user:secret@proxy.example:8443/path?q=1#frag");
55
+ // → "https://proxy.example:8443/path"
56
+ ```
57
+
58
+ ### `resolveProxyTlsCa(options): string | undefined`
59
+
60
+ Resolves a `ProxylineTlsOptions` value to a PEM string by reading `caFile` from disk if needed. Returns `undefined` when no CA material is supplied. Exposed so callers can pre-resolve before passing values into their own TLS-using code.
61
+
62
+ ## Classes
63
+
64
+ ### `ProxylineError extends Error`
65
+
66
+ ```ts
67
+ class ProxylineError extends Error {
68
+ readonly code: string;
69
+ readonly name: "ProxylineError";
70
+ }
71
+ ```
72
+
73
+ Codes:
74
+
75
+ - `MANAGED_PROXY_URL_REQUIRED` — `mode: "managed"` was used without `proxyUrl`.
76
+ - `UNSUPPORTED_PROXY_PROTOCOL` — proxy URL scheme is not `http://` or `https://`.
77
+ - `RUNTIME_ALREADY_ACTIVE` — another Proxyline runtime is already installed.
78
+ - `CONNECT_FAILED` — `openProxyConnectTunnel` failed (bad response, timeout, header overrun, or socket error).
79
+ - `INVALID_CONNECT_TARGET` — `openProxyConnectTunnel` received an empty or unsafe target host, invalid bracket syntax, or an invalid target port.
80
+
81
+ ## Types
82
+
83
+ ### `ProxylineMode`
84
+
85
+ ```ts
86
+ type ProxylineMode = "managed" | "ambient";
87
+ ```
88
+
89
+ See [Modes](./modes.md).
90
+
91
+ ### `ProxylineSurface`
92
+
93
+ ```ts
94
+ type ProxylineSurface =
95
+ | "node-http"
96
+ | "node-https"
97
+ | "undici"
98
+ | "websocket"
99
+ | "connect"
100
+ | "unknown";
101
+ ```
102
+
103
+ Used in `explain()` decisions and event payloads to identify which network surface a decision is for. Pass it via `explain(url, { surface })`.
104
+
105
+ ### `ProxylineOptions`
106
+
107
+ ```ts
108
+ type ProxylineOptions = Readonly<{
109
+ mode: ProxylineMode;
110
+ proxyUrl?: string | URL;
111
+ proxyTls?: ProxylineTlsOptions;
112
+ bypassPolicy?: ProxylineBypassPolicy;
113
+ ifActive?: "error" | "reuse-compatible" | "replace";
114
+ onEvent?: (event: ProxylineEvent) => void;
115
+ undici?: ProxylineUndiciOptions;
116
+ }>;
117
+ ```
118
+
119
+ - `mode` — required. `"managed"` or `"ambient"`.
120
+ - `proxyUrl` — required in managed mode, ignored in ambient mode. Managed-mode URLs must be `http://` or `https://`.
121
+ - `proxyTls` — CA trust scoped to the proxy endpoint. See [Proxy TLS](./proxy-tls.md).
122
+ - `bypassPolicy` — managed-mode escape hatch for trusted traffic that should go direct. Ignored in ambient mode.
123
+ - `ifActive` — process singleton behavior when Proxyline is already active. Defaults to `"error"`. `"reuse-compatible"` returns the active handle when mode/proxy/TLS/bypass/undici settings match; ambient mode also requires the captured proxy env snapshot to match. `"replace"` stops the active runtime before installing the new one.
124
+ - `onEvent` — callback fired with every `ProxylineEvent`.
125
+ - `undici` — default options for Proxyline-owned undici dispatchers.
126
+
127
+ ### `ProxylineUndiciOptions`
128
+
129
+ ```ts
130
+ type ProxylineUndiciOptions = Readonly<{
131
+ allowH2?: boolean;
132
+ bodyTimeout?: number;
133
+ headersTimeout?: number;
134
+ connect?: Readonly<{
135
+ autoSelectFamily?: boolean;
136
+ autoSelectFamilyAttemptTimeout?: number;
137
+ }>;
138
+ }>;
139
+ ```
140
+
141
+ These options apply to the global dispatcher installed by `installGlobalProxy()` and to dispatchers returned by `createUndiciDispatcher()`.
142
+
143
+ ### `ProxylineTlsOptions`
144
+
145
+ ```ts
146
+ type ProxylineTlsOptions = Readonly<{
147
+ ca?: string; // PEM string
148
+ caFile?: string; // path read with fs.readFileSync(..., "utf8")
149
+ }>;
150
+ ```
151
+
152
+ When both are provided, `ca` wins.
153
+
154
+ ### `ProxylineDecision`
155
+
156
+ ```ts
157
+ type ProxylineDecision = Readonly<{
158
+ kind: "proxied" | "direct" | "blocked";
159
+ reason: string;
160
+ surface: ProxylineSurface;
161
+ url: string;
162
+ proxyUrl?: string; // redacted
163
+ }>;
164
+ ```
165
+
166
+ Known `reason` values:
167
+
168
+ - `"managed-proxy-active"` — managed mode applied.
169
+ - `"managed-proxy-bypass-policy"` — managed mode was active, but `bypassPolicy` intentionally sent the URL direct.
170
+ - `"managed-proxy-unsupported-url-scheme"` — managed mode is active, but the URL scheme is not one Proxyline can proxy.
171
+ - `"ambient-proxy-active"` — ambient mode resolved a proxy from env.
172
+ - `"ambient-proxy-not-configured"` — ambient mode has no proxy env set, or the URL scheme is unsupported.
173
+ - `"no-proxy-match"` — the URL matched `NO_PROXY`.
174
+ - `"runtime-stopped"` — `explain()` was called after `stop()`.
175
+
176
+ `kind: "blocked"` is reserved for future explicit deny rules; the current implementation does not produce blocked decisions.
177
+
178
+ ### `ProxylineBypassRequest`
179
+
180
+ ```ts
181
+ type ProxylineBypassRequest = Readonly<{
182
+ surface: ProxylineSurface;
183
+ url: string;
184
+ }>;
185
+ ```
186
+
187
+ ### `ProxylineBypassPolicy`
188
+
189
+ ```ts
190
+ type ProxylineBypassPolicy = (request: ProxylineBypassRequest) => boolean;
191
+ ```
192
+
193
+ Return `true` to send that managed-mode request direct. Use this only for explicitly trusted destinations such as loopback control-plane endpoints.
194
+
195
+ ### `ProxylineEvent`
196
+
197
+ ```ts
198
+ type ProxylineEvent =
199
+ | Readonly<{ type: "runtime.installed"; mode: ProxylineMode; active: boolean; proxyUrl?: string }>
200
+ | Readonly<{ type: "runtime.stopped"; mode: ProxylineMode }>
201
+ | Readonly<{ type: "decision"; decision: ProxylineDecision }>
202
+ | Readonly<{ type: "warning"; code: string; message: string }>;
203
+ ```
204
+
205
+ `decision` events fire from inside `explain()`. `runtime.installed` and `runtime.stopped` fire from `installProxyline` and `handle.stop()` respectively. `warning` is reserved for future runtime diagnostics.
206
+
207
+ ### `ExplainOptions`
208
+
209
+ ```ts
210
+ type ExplainOptions = Readonly<{
211
+ surface?: ProxylineSurface;
212
+ }>;
213
+ ```
214
+
215
+ ### `ProxylineHandle`
216
+
217
+ ```ts
218
+ type ProxylineHandle = Readonly<{
219
+ mode: ProxylineMode;
220
+ active: boolean;
221
+ proxyUrl?: string;
222
+ createNodeAgent: () => http.Agent;
223
+ createUndiciDispatcher: () => Dispatcher;
224
+ createWebSocketAgent: () => http.Agent;
225
+ explain: (url: string | URL, options?: ExplainOptions) => ProxylineDecision;
226
+ registerBypass: (registration: ProxylineBypassRegistration) => () => void;
227
+ stop: () => void;
228
+ withBypass: <T>(registration: ProxylineBypassRegistration, run: () => T) => T;
229
+ }>;
230
+ ```
231
+
232
+ - `mode` — the mode this handle was installed with.
233
+ - `active` — `true` when the runtime is installed and forcing/respecting a proxy.
234
+ - `proxyUrl` — redacted proxy URL string when active.
235
+ - `createNodeAgent()` — proxy-aware `http.Agent` for ad-hoc node:http(s) use. Returns a direct agent when inactive or after `stop()`.
236
+ - `createUndiciDispatcher()` — proxy-aware undici `Dispatcher`. Returns a direct `UndiciAgent()` when ambient-inactive or after `stop()`.
237
+ - `createWebSocketAgent()` — same as `createNodeAgent()` but typed for WebSocket clients.
238
+ - `explain(url, options?)` — returns a `ProxylineDecision` and emits a `decision` event.
239
+ - `registerBypass({ url, surface? })` — managed-mode process-wide bypass. Returns an unregister callback. When `surface` is omitted, the bypass matches any surface for that exact URL.
240
+ - `stop()` — restores the captured Node HTTP(S) stack, undici dispatcher, and fetch globals, destroys Proxyline-owned runtime agents/dispatchers, emits `runtime.stopped`. Idempotent.
241
+ - `withBypass(registration, run)` — applies a bypass only to `run()` and async work created inside that callback.
242
+
243
+ ### Dispatcher Detection
244
+
245
+ ```ts
246
+ isProxylineDispatcher(dispatcher: unknown): boolean;
247
+ ```
248
+
249
+ Returns `true` for Proxyline-owned managed and ambient undici dispatchers. Use this instead of constructor-name checks when integrating with code that also manages undici globals. Import from `@openclaw/proxyline/dispatcher-brand` when detection must not load Proxyline's full runtime.
250
+
251
+ ### `OpenProxyConnectTunnelOptions`
252
+
253
+ ```ts
254
+ type OpenProxyConnectTunnelOptions = Readonly<{
255
+ proxyUrl: string | URL;
256
+ proxyTls?: ProxylineTlsOptions;
257
+ targetHost: string;
258
+ targetPort: number;
259
+ timeoutMs?: number;
260
+ }>;
261
+ ```
262
+
263
+ - `proxyUrl` — `http://` or `https://`. Userinfo becomes a `Proxy-Authorization: Basic` header.
264
+ - `proxyTls` — CA trust for HTTPS proxies. See [Proxy TLS](./proxy-tls.md).
265
+ - `targetHost` / `targetPort` — what to ask the proxy to connect to.
266
+ - `timeoutMs` — overall budget for the CONNECT handshake.
267
+
268
+ ### `AmbientNodeProxyAgentOptions`
269
+
270
+ ```ts
271
+ type AmbientNodeProxyAgentOptions = {
272
+ env?: ProxyEnvSnapshot;
273
+ protocol?: "http" | "https";
274
+ proxyTls?: ProxylineTlsOptions;
275
+ };
276
+ ```
277
+
278
+ - `env` — optional env snapshot. Defaults to reading process env.
279
+ - `protocol` — probe protocol, defaulting to `"https"`.
280
+ - `proxyTls` — CA trust for HTTPS proxy endpoints. See [Proxy TLS](./proxy-tls.md).
@@ -0,0 +1,70 @@
1
+ # Environment Variables
2
+
3
+ Ambient mode reads its configuration from the environment at install time. Managed mode does not.
4
+
5
+ ## Variables
6
+
7
+ | Variable | Used for | Notes |
8
+ | --- | --- | --- |
9
+ | `HTTP_PROXY` | proxy for `http:` and `ws:` URLs | lower case wins when both are set |
10
+ | `HTTPS_PROXY` | proxy for `https:` and `wss:` URLs | |
11
+ | `ALL_PROXY` | fallback when the protocol-specific variable is unset or unsupported | |
12
+ | `NO_PROXY` | comma- or whitespace-separated list of exemptions | matched against the destination URL |
13
+ | `http_proxy` | lowercase alias for `HTTP_PROXY` | takes precedence over uppercase |
14
+ | `https_proxy` | lowercase alias | |
15
+ | `all_proxy` | lowercase alias | |
16
+ | `no_proxy` | lowercase alias | |
17
+
18
+ Empty or whitespace-only values are treated as unset.
19
+
20
+ ## Activation
21
+
22
+ The ambient runtime is **active** when any of `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY` (or their lowercase forms) is set to a supported `http://` or `https://` proxy endpoint. Bare endpoints count because Proxyline defaults them to `http://`.
23
+
24
+ `NO_PROXY` alone does not activate the runtime. With only `NO_PROXY` set, `proxy.active` is `false` and Proxyline installs no patches.
25
+
26
+ ## Snapshot at install
27
+
28
+ The values are read **once** at `installProxyline` time. Changing `process.env.HTTP_PROXY` afterwards does not retroactively activate or reconfigure Proxyline. Call `proxy.stop()` and re-install if the environment changes.
29
+
30
+ ## Proxy URL parsing
31
+
32
+ - Bare endpoints (no scheme) default to `http://`. `HTTPS_PROXY=proxy.corp:8080` becomes `http://proxy.corp:8080`.
33
+ - `http://` and `https://` are the only accepted schemes. Anything else is ignored.
34
+ - Userinfo (`user:pass@`) in the proxy URL becomes a `Proxy-Authorization: Basic` header.
35
+
36
+ ## NO_PROXY matching
37
+
38
+ Entries are split on commas and whitespace. Matching rules, in order:
39
+
40
+ 1. `*` alone matches everything. The URL goes direct.
41
+ 2. Entries can include a port: `internal.corp:8443`. When present, the entry only matches the URL's port (defaulting to `80` for `http`/`ws` and `443` for `https`/`wss`).
42
+ 3. Entries starting with `.` or `*` are suffix matches. `.corp.example` matches `api.corp.example` and `corp.example`. `*.corp.example` is equivalent.
43
+ 4. Other entries are exact host matches.
44
+ 5. IPv6 entries may be bracketed (`[::1]:8443`) or bare (`::1`). Bracketed forms are stripped before comparison.
45
+ 6. Hostnames are lowercased and trailing dots are stripped before comparison.
46
+
47
+ When a URL matches `NO_PROXY`, `explain()` returns `kind: "direct"`, `reason: "no-proxy-match"`.
48
+
49
+ ## Examples
50
+
51
+ ```bash
52
+ # go through the proxy for everything except internal hosts
53
+ export HTTPS_PROXY="https://proxy.corp.example:8443"
54
+ export NO_PROXY=".corp.example,localhost,127.0.0.1,::1"
55
+ ```
56
+
57
+ ```bash
58
+ # only https traffic via proxy, http stays direct
59
+ export HTTPS_PROXY="https://proxy.corp.example:8443"
60
+ ```
61
+
62
+ ```bash
63
+ # fall through to ALL_PROXY for everything
64
+ export ALL_PROXY="socks-not-supported://example" # ignored — only http/https proxies are honored
65
+ export ALL_PROXY="http://gateway.corp:3128" # used for both http and https URLs
66
+ ```
67
+
68
+ ## Interaction with `fetch`
69
+
70
+ The undici global dispatcher is installed with Proxyline's own ambient resolver over the same `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` snapshot Proxyline read at install time. `fetch` and `node:http` therefore agree on the proxy decision for any URL.