@openclaw/proxyline 0.2.0 → 0.3.1

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.
@@ -0,0 +1,66 @@
1
+ # Proxy TLS
2
+
3
+ `proxyTls` scopes CA trust **for the proxy endpoint only**. It does not loosen TLS for the destination, and it does not affect Node's default trust store.
4
+
5
+ ## Why scope CA trust
6
+
7
+ Corporate proxies often present TLS certificates signed by a private CA. Two common approaches that Proxyline avoids:
8
+
9
+ - Setting `NODE_EXTRA_CA_CERTS` — process-wide trust change, affects every TLS handshake, easy to forget.
10
+ - Setting `NODE_TLS_REJECT_UNAUTHORIZED=0` — disables verification entirely, destroys security.
11
+
12
+ Proxyline instead supplies a `ca` value **only** to the agent that talks to the proxy. Destination handshakes continue using the system trust store and the caller's `ca` / `rejectUnauthorized` settings.
13
+
14
+ ## Options
15
+
16
+ ```ts
17
+ type ProxylineTlsOptions = Readonly<{
18
+ ca?: string; // PEM contents
19
+ caFile?: string; // path; read with fs.readFileSync(path, "utf8")
20
+ }>;
21
+ ```
22
+
23
+ Pass exactly one. `ca` wins when both are provided. The resolved PEM is forwarded to:
24
+
25
+ - The internal Proxyline Node agent used for `node:http` and `node:https`.
26
+ - Proxyline's managed undici dispatcher or ambient dispatcher.
27
+ - The `tls.connect` call inside `openProxyConnectTunnel` when the proxy URL is `https://`.
28
+
29
+ ## Recipes
30
+
31
+ ### From disk
32
+
33
+ ```ts
34
+ installGlobalProxy({
35
+ mode: "managed",
36
+ proxyUrl: "https://proxy.corp.example:8443",
37
+ proxyTls: { caFile: "/etc/proxy-ca.pem" },
38
+ });
39
+ ```
40
+
41
+ ### Inline PEM
42
+
43
+ ```ts
44
+ import { readFileSync } from "node:fs";
45
+
46
+ const ca = readFileSync("/etc/proxy-ca.pem", "utf8");
47
+
48
+ installGlobalProxy({
49
+ mode: "managed",
50
+ proxyUrl: "https://proxy.corp.example:8443",
51
+ proxyTls: { ca },
52
+ });
53
+ ```
54
+
55
+ ### Pre-resolving for your own code
56
+
57
+ ```ts
58
+ import { resolveProxyTlsCa } from "@openclaw/proxyline";
59
+
60
+ const ca = resolveProxyTlsCa({ caFile: "/etc/proxy-ca.pem" });
61
+ // ca is a PEM string, or undefined if no options were supplied
62
+ ```
63
+
64
+ ## Destination TLS
65
+
66
+ Destination TLS is independent of `proxyTls`. When you call `https.request(url, { ca, rejectUnauthorized, ... })`, those options apply to the destination handshake exactly as Node would normally apply them. Proxyline only lifts them off a caller-supplied `agent` so they survive the agent replacement; see [Surfaces — TLS identity preservation](./surfaces.md#tls-identity-preservation).
@@ -0,0 +1,83 @@
1
+ # Security
2
+
3
+ Proxyline is a Node-process runtime, not an operating-system sandbox. Understanding what it does — and what it cannot do — is essential before relying on it as a policy.
4
+
5
+ ## Assurance model
6
+
7
+ Proxyline's runtime assurances assume it is installed before application and plugin networking code is loaded. Install it as the first import in the process when proxy routing is part of the security posture.
8
+
9
+ These assurances apply to the surfaces Proxyline patches or helpers it creates. They do not apply to code that captured networking functions before installation, raw sockets, native transports, or separate bundled transport stacks.
10
+
11
+ ## What Proxyline enforces
12
+
13
+ In **managed** mode, Proxyline forces traffic through the configured proxy on the surfaces it covers:
14
+
15
+ - `http.request` / `http.get` / `http.globalAgent`
16
+ - `https.request` / `https.get` / `https.globalAgent`
17
+ - The undici global dispatcher (i.e. `fetch`)
18
+ - WebSocket clients that accept a Node `agent`, via `proxy.createWebSocketAgent()` or by reusing the patched `http.request` during the upgrade
19
+ - Explicit HTTP CONNECT when callers opt in to `openProxyConnectTunnel`
20
+
21
+ Caller-supplied `http.Agent` / `https.Agent` instances are replaced per request, and `createConnection` overrides are stripped. TLS-relevant agent options are lifted onto the request so destination TLS still validates correctly.
22
+
23
+ Managed mode can also accept a `bypassPolicy` callback for explicitly trusted direct-routing exceptions, such as local control-plane endpoints. Treat it as part of your security policy: keep it narrow and log matching URLs with `explain()`.
24
+
25
+ In **ambient** mode the same surfaces are covered when at least one supported `http://` or `https://` proxy endpoint is set through `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`; unsupported proxy schemes are ignored, and `NO_PROXY` is honored.
26
+
27
+ ## What Proxyline cannot enforce
28
+
29
+ Anything that does not flow through the patched APIs:
30
+
31
+ - Direct `net.connect` or `tls.connect` calls. Code that opens raw sockets is not seen by Proxyline.
32
+ - Native modules with private transport stacks (e.g. some database drivers, gRPC C bindings).
33
+ - Libraries that import and call `undici.fetch` directly with their own explicit `Dispatcher`. Proxyline's managed `globalThis.fetch` strips explicit dispatchers, but it cannot rewrite every imported undici function reference.
34
+ - DNS resolution itself. Proxyline tells the proxy a hostname; DNS-based exfiltration via the local resolver is out of scope.
35
+ - Sockets opened **before** `installProxyline` ran. Existing keepalive connections continue to use whatever transport they were created with.
36
+ - Module references captured before `installProxyline` ran. Anything that stored `http.request` in a local variable at import time keeps the un-patched reference.
37
+ - Non-standard internals on native `Request` objects created before `installProxyline` ran. Proxyline can normalize standard Request fields, but runtimes do not expose every internal field consistently.
38
+
39
+ For a process-wide proxy enforcement policy, combine Proxyline with operating-system level controls (egress firewall rules, network namespaces, seccomp, or a sidecar proxy).
40
+
41
+ ## Install order matters
42
+
43
+ Install Proxyline as the **first** import in your application entry point. Anything that imports `node:http`, `node:https`, `undici`, or a wrapper library before Proxyline is initialized may have already captured the original method references. The patches still apply to future calls into `http.request` etc., but a stale captured reference will bypass them.
44
+
45
+ ```ts
46
+ // entry.ts
47
+ import { installGlobalProxy } from "@openclaw/proxyline";
48
+
49
+ installGlobalProxy({ mode: "managed", proxyUrl: process.env.PROXY_URL! });
50
+
51
+ // only after install:
52
+ await import("./app.js");
53
+ ```
54
+
55
+ ## Process-wide singleton
56
+
57
+ Only one Proxyline runtime can be installed at a time. A second `installProxyline` call throws `ProxylineError` with code `RUNTIME_ALREADY_ACTIVE` by default. Use `ifActive: "reuse-compatible"` to share the current matching runtime, `ifActive: "replace"` to stop and reinstall intentionally, or call `proxy.stop()` yourself before installing a different runtime.
58
+
59
+ This is deliberate: two competing proxy patches would race on `http.request` and `globalAgent`, and the loser would silently bypass the winner.
60
+
61
+ ## Credential handling
62
+
63
+ - Userinfo in proxy URLs becomes a `Proxy-Authorization: Basic` header on every CONNECT / absolute-form request.
64
+ - Userinfo, search, and fragment are stripped from any URL Proxyline reports back to the caller (`handle.proxyUrl`, `decision.proxyUrl`, `runtime.installed`).
65
+ - `redactProxyUrl` is exported so callers can apply the same rule to URLs they log themselves.
66
+
67
+ ## Threat model summary
68
+
69
+ | Threat | Mitigated | Notes |
70
+ | --- | --- | --- |
71
+ | Library passes a direct `http.Agent` per request | yes (managed) | Replaced before the request runs |
72
+ | Library passes a direct `Dispatcher` to managed `globalThis.fetch` | yes | Explicit dispatchers are stripped |
73
+ | Trusted local endpoint needs direct routing | yes, with `bypassPolicy` | Keep the callback narrow and auditable |
74
+ | Library calls imported `undici.fetch` with a direct `Dispatcher` | no | Imported function references are outside the global fetch patch |
75
+ | Library uses `net.connect` directly | no | Out of scope |
76
+ | Library captured `http.request` at import time | no (if before install) | Install Proxyline first |
77
+ | Environment variable set after install | no | Snapshot at install time |
78
+ | Proxy credentials leaked into logs | yes (in Proxyline output) | Use `redactProxyUrl` for caller logs |
79
+ | Process-wide CA trust drift | yes | `proxyTls` is scoped to the proxy endpoint |
80
+
81
+ ## Reporting
82
+
83
+ Security issues: open a private advisory at <https://github.com/openclaw/proxyline/security/advisories>. Do not open public issues for unpatched problems.
@@ -0,0 +1,116 @@
1
+ # Surfaces
2
+
3
+ Proxyline installs at the layer above each network API. It does not own sockets, except via the explicit `openProxyConnectTunnel` helper.
4
+
5
+ ## node:http and node:https
6
+
7
+ Both modules have their request entry points patched and their `globalAgent` replaced.
8
+
9
+ Patched methods:
10
+
11
+ - `http.request`, `http.get`
12
+ - `https.request`, `https.get`
13
+
14
+ Per request, Proxyline:
15
+
16
+ 1. Copies the call's options object so caller state is not mutated.
17
+ 2. Reads TLS-relevant options off any caller-supplied `agent` and lifts them onto the request options (see [TLS identity preservation](#tls-identity-preservation)).
18
+ 3. Sets `servername` to the destination hostname when not already set and the hostname is not an IP literal.
19
+ 4. Builds a fresh Proxyline Node agent for the request and assigns it as `options.agent`.
20
+ 5. Deletes any `createConnection` override so callers cannot punch through to the kernel directly.
21
+ 6. Destroys the per-request proxy agent when the request closes.
22
+
23
+ This means caller agents are not just ignored — the per-request agent is replaced before the original method runs, so even libraries that read `req.agent` after construction see the proxy agent.
24
+
25
+ ### TLS identity preservation
26
+
27
+ When the caller supplied an `https.Agent` with TLS options, the following keys are lifted into the request so the destination TLS handshake still validates correctly:
28
+
29
+ `ca`, `cert`, `ciphers`, `clientCertEngine`, `crl`, `dhparam`, `ecdhCurve`, `honorCipherOrder`, `key`, `maxVersion`, `minVersion`, `passphrase`, `pfx`, `rejectUnauthorized`, `secureOptions`, `secureProtocol`, `sessionIdContext`.
30
+
31
+ The destination `servername` is also inferred from the URL or `hostname`/`host` options when the caller did not set one. IP literals are not used as SNI values.
32
+
33
+ ### Absolute-form requests
34
+
35
+ Some HTTP clients build absolute-form requests themselves (e.g. `path: "https://api.example.com/graphql"`). Proxyline leaves the path intact and forwards it through the proxy unchanged, so libraries that already implement their own proxy handling continue to function.
36
+
37
+ ## undici and fetch
38
+
39
+ `installGlobalProxy` calls `undici.setGlobalDispatcher` and patches `globalThis.fetch` to use Proxyline's dispatcher with:
40
+
41
+ - Proxyline's managed dispatcher in managed mode, backed by `undici.ProxyAgent` instances pointed at `proxyUrl` and trusting `proxyTls` when supplied.
42
+ - Proxyline's ambient dispatcher in ambient mode, resolving each request against the current install-time `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` snapshot, with the same `proxyTls`.
43
+
44
+ The original dispatcher and fetch globals are captured and restored on `stop()`.
45
+ Pass `undici` options to tune Proxyline-owned dispatcher defaults such as `bodyTimeout`, `headersTimeout`, `allowH2`, and `connect.autoSelectFamily`.
46
+
47
+ ```ts
48
+ import { fetch } from "undici";
49
+
50
+ await fetch("https://api.example.com/health"); // routed through Proxyline
51
+ ```
52
+
53
+ In managed mode, Proxyline's patched `globalThis.fetch` ignores explicit `dispatcher` options so a per-call undici `Agent` cannot bypass the managed proxy. In ambient mode, and for callers using imported `undici.fetch` directly, an explicit undici `Agent` or `Dispatcher` still wins. Use `proxy.createUndiciDispatcher()` to get a dispatcher pre-wired to the same policy.
54
+
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
+
57
+ ## WebSocket
58
+
59
+ WebSocket clients that accept a Node `agent` option (e.g. `ws`) can route through the proxy via:
60
+
61
+ ```ts
62
+ import WebSocket from "ws";
63
+
64
+ const socket = new WebSocket("wss://events.example.com/", {
65
+ agent: proxy.createWebSocketAgent(),
66
+ });
67
+ ```
68
+
69
+ When ambient mode is inactive, `createWebSocketAgent()` returns a direct-routing agent, so calling code does not need a conditional path.
70
+
71
+ Clients that do **not** expose an `agent` option but still route their handshake through `http.request` are covered automatically by the global patch.
72
+
73
+ ## HTTP CONNECT tunnel
74
+
75
+ Some libraries — notably HTTP/2 clients — need ownership of the underlying socket. `openProxyConnectTunnel` performs an `HTTP CONNECT` against the proxy and resolves with a connected `net.Socket` (or `tls.TLSSocket` for HTTPS proxies).
76
+
77
+ ```ts
78
+ import { openProxyConnectTunnel } from "@openclaw/proxyline";
79
+
80
+ const socket = await openProxyConnectTunnel({
81
+ proxyUrl: "https://proxy.corp.example:8443",
82
+ proxyTls: { caFile: "/etc/proxy-ca.pem" },
83
+ targetHost: "api.example.com",
84
+ targetPort: 443,
85
+ timeoutMs: 2_000,
86
+ });
87
+ ```
88
+
89
+ Properties:
90
+
91
+ - HTTP and HTTPS proxy endpoints supported. Other schemes throw `UNSUPPORTED_PROXY_PROTOCOL`.
92
+ - HTTPS proxies use ALPN `http/1.1`. SNI is the proxy hostname unless that is an IP literal.
93
+ - Userinfo in the `proxyUrl` becomes a `Proxy-Authorization: Basic ...` header.
94
+ - A bounded `16 KiB` header buffer protects against malicious or runaway proxy responses.
95
+ - `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry.
96
+ - Bytes the proxy sends after the response headers are re-injected with `socket.unshift()` so the caller sees the full target stream.
97
+ - Non-2xx status lines, header overrun, premature close, and socket errors are all surfaced as `ProxylineError` with code `CONNECT_FAILED`.
98
+
99
+ The helper is standalone — it does not require `installGlobalProxy` to have been called.
100
+
101
+ ## Caller-built agents
102
+
103
+ A `http.Agent` or `https.Agent` you construct yourself is **replaced** in managed mode and **replaced when active** in ambient mode. The replacement happens inside the patched `http.request` / `https.request` call: by the time the underlying request starts, `options.agent` already points at a proxy agent.
104
+
105
+ This is intentional: a common bypass is `new https.Agent({ keepAlive: true })` passed per-request. Without replacement the proxy is silently skipped.
106
+
107
+ To get a proxy-aware agent without going through the patched globals, use:
108
+
109
+ - `proxy.createNodeAgent()` — an `http.Agent` that proxies HTTP and HTTPS requests.
110
+ - `createAmbientNodeProxyAgent()` — a standalone ambient helper that returns an agent only when proxy env applies.
111
+ - `proxy.createUndiciDispatcher()` — an undici `Dispatcher` mirroring the current mode.
112
+ - `proxy.createWebSocketAgent()` — an `http.Agent` suitable for WebSocket upgrades.
113
+
114
+ Helper-created agents and dispatchers are caller-owned. Destroy or close them when the caller is done.
115
+
116
+ When the runtime is inactive (ambient mode with no env proxy set), these helpers return direct agents/dispatchers.
@@ -0,0 +1,78 @@
1
+ # Testing
2
+
3
+ Proxyline ships with an in-process **proxy lab** used by its end-to-end tests. The lab spins up a target HTTP(S) server and a forward proxy on random ports, then asserts that requests routed through Proxyline reach the proxy with the right shape.
4
+
5
+ The lab lives in `test/support/proxy-lab.ts`. It is not exported from the package — copy it into your own test suite if you find it useful.
6
+
7
+ ## Run the suite
8
+
9
+ ```bash
10
+ pnpm check # build + typecheck + coverage-gated tests
11
+ pnpm test # build + unit, integration, and package-entrypoint tests
12
+ pnpm coverage # build + tests with native Node source coverage
13
+ pnpm typecheck # type-only
14
+ ```
15
+
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
+ `pnpm check` enforces native Node coverage for `src/**/*.ts` with thresholds of 85% lines, 80% branches, and 80% functions on Node versions that expose native threshold flags.
18
+ CI runs that check on Ubuntu, macOS, and Windows across Node 20.18.1, 22, 24, and 26; Node 20.18.1 keeps the declared minimum covered with native coverage enabled and skips only the unavailable threshold/include flags.
19
+
20
+ ## What the lab covers
21
+
22
+ The lab proxy:
23
+
24
+ - Accepts absolute-form HTTP proxy requests and HTTPS CONNECT tunnels.
25
+ - Denies configured paths (e.g. `/denied` returns `403`).
26
+ - Blocks loopback authorities by default with an explicit allowlist for the lab's own target.
27
+ - Optionally requires a `Proxy-Authorization` header.
28
+ - Optionally runs over HTTPS with a freshly generated CA.
29
+
30
+ The lab target:
31
+
32
+ - Serves `/allowed` (`200`) and `/denied` (`200` if reached, which the proxy is supposed to prevent).
33
+ - Optionally runs over HTTPS with a generated certificate scoped to the test host.
34
+
35
+ Test coverage exercises:
36
+
37
+ - Absolute-form HTTP proxy requests
38
+ - HTTP CONNECT tunneling
39
+ - Path denial via the proxy
40
+ - Loopback blocking with explicit allowlists
41
+ - HTTPS proxy endpoints with scoped CA trust
42
+ - node:http global routing
43
+ - Caller-supplied agent override (managed mode)
44
+ - Managed-mode `bypassPolicy`
45
+ - HTTPS caller agent TLS option preservation
46
+ - undici/fetch routing
47
+ - WebSocket agent routing
48
+ - Forced upgrade through the patched `http.request`
49
+ - Explicit CONNECT socket routing
50
+ - Ambient `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (with credentials), lowercase variants, `NO_PROXY` exemptions, IPv6 bypass entries, bare endpoints
51
+
52
+ ## Writing tests against Proxyline
53
+
54
+ A minimal pattern:
55
+
56
+ ```ts
57
+ import test from "node:test";
58
+ import assert from "node:assert/strict";
59
+ import { installGlobalProxy } from "@openclaw/proxyline";
60
+
61
+ test("explain says the proxy is being used", () => {
62
+ const proxy = installGlobalProxy({
63
+ mode: "managed",
64
+ proxyUrl: "http://127.0.0.1:1",
65
+ });
66
+ try {
67
+ const decision = proxy.explain("https://api.example.com/health");
68
+ assert.equal(decision.kind, "proxied");
69
+ assert.equal(decision.reason, "managed-proxy-active");
70
+ } finally {
71
+ proxy.stop();
72
+ }
73
+ });
74
+ ```
75
+
76
+ Always call `stop()` in `finally`. The runtime is process-global; a leaked install causes the next test's `installProxyline` to throw `RUNTIME_ALREADY_ACTIVE`.
77
+
78
+ For ambient-mode tests, scope environment changes with a helper that snapshots and restores `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` (and their lowercase forms). See `test/e2e.test.ts` for the `withProxyEnv` helper used by the suite.
@@ -0,0 +1,71 @@
1
+ # Troubleshooting
2
+
3
+ ## `ProxylineError: MANAGED_PROXY_URL_REQUIRED`
4
+
5
+ You used `mode: "managed"` without `proxyUrl`. Managed mode does not fall back to the environment. Supply a URL or switch to `mode: "ambient"`.
6
+
7
+ ## `ProxylineError: UNSUPPORTED_PROXY_PROTOCOL`
8
+
9
+ `proxyUrl` is not `http://` or `https://`. SOCKS and other transports are not supported.
10
+
11
+ ## `ProxylineError: RUNTIME_ALREADY_ACTIVE`
12
+
13
+ Two parts of your code called `installProxyline` without an intervening `stop()` or compatible `ifActive` policy. Find the existing handle and reuse it, pass `ifActive: "reuse-compatible"` when the settings should match, or call `stop()` before the second install. Common causes: test setup that re-installs on every test, double-invoked entry points, hot-reload tooling that re-evaluates the entry module.
14
+
15
+ ## `ProxylineError: CONNECT_FAILED`
16
+
17
+ Returned by `openProxyConnectTunnel`. Inspect the message for the immediate cause:
18
+
19
+ - `proxy CONNECT timed out after Nms` — bump `timeoutMs` or check connectivity to the proxy.
20
+ - A status line such as `HTTP/1.1 407 Proxy Authentication Required` — include credentials in `proxyUrl`.
21
+ - A status line such as `HTTP/1.1 403 Forbidden` — the proxy refused the destination.
22
+ - `proxy socket closed before CONNECT response` — the proxy disconnected. Often a TLS mismatch; verify `proxyTls.ca`.
23
+ - `proxy CONNECT response headers exceeded 16384 bytes` — defensive cap. Unexpected from a normal proxy.
24
+
25
+ ## fetch / undici still goes direct
26
+
27
+ - In ambient mode, a library may be using its own `Dispatcher` and passing it to `fetch` explicitly. Inspect the `dispatcher` option; it overrides the global one outside managed `globalThis.fetch`. Use `proxy.createUndiciDispatcher()` if you need a Proxyline-aware dispatcher.
28
+ - The library was loaded **before** `installProxyline`. Some libraries cache a dispatcher at import time. Install Proxyline first.
29
+
30
+ ## Caller agent is being ignored
31
+
32
+ That is the intended managed-mode behavior — caller agents are replaced per request. TLS-relevant options (`ca`, `key`, `cert`, `rejectUnauthorized`, `minVersion`, `maxVersion`, `ciphers`, …) are lifted off the caller's agent so destination TLS still validates. If your option is missing, see [Surfaces — TLS identity preservation](./surfaces.md#tls-identity-preservation) for the full list of preserved keys.
33
+
34
+ ## "ECONNREFUSED" against the proxy
35
+
36
+ Verify the proxy is reachable from this process:
37
+
38
+ ```bash
39
+ curl -v -x "$HTTPS_PROXY" https://api.example.com
40
+ ```
41
+
42
+ If `curl` succeeds but Node fails, the most common causes are:
43
+
44
+ - Wrong scheme (`HTTPS_PROXY=proxy.corp:8080` defaults to `http://proxy.corp:8080`).
45
+ - A private CA presented by the proxy that Node does not trust. Use `proxyTls`.
46
+ - Code captured `http.request` before Proxyline installed. Install Proxyline first.
47
+
48
+ ## TLS errors against the proxy
49
+
50
+ The proxy presents a certificate Node does not trust. Pass `proxyTls.caFile` (or `proxyTls.ca`) — see [Proxy TLS](./proxy-tls.md). Do not disable TLS verification process-wide.
51
+
52
+ ## TLS errors against the destination
53
+
54
+ Proxyline does not modify destination TLS. Use the same `ca` / `cert` / `key` options you would use without a proxy. When passed via an `https.Agent`, Proxyline copies them onto the request automatically.
55
+
56
+ ## Tests interfere with each other
57
+
58
+ Each test should call `proxy.stop()` in its teardown. Without that, the next install throws `RUNTIME_ALREADY_ACTIVE` and the rest of the suite fails. The patches and the undici dispatcher are global, so leaks affect every concurrent test as well.
59
+
60
+ ## Ambient mode reports `active: false`
61
+
62
+ No supported proxy variables are set. Check `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (and their lowercase forms), and make sure their values use `http://` or `https://`. `NO_PROXY` alone is not enough to activate the runtime. See [Environment Variables](./environment-variables.md).
63
+
64
+ ## `NO_PROXY` is not matching as expected
65
+
66
+ - Suffix matches need a leading `.` or `*`. `corp.example` matches only the exact host; `.corp.example` matches `api.corp.example`.
67
+ - A port suffix (`internal.corp:8443`) restricts the match to that port.
68
+ - IPv6 hosts may be bracketed or bare (`[::1]`, `::1`).
69
+ - Hostnames are lowercased and trailing dots are stripped before comparison.
70
+
71
+ When in doubt, use `proxy.explain(url)` — `reason: "no-proxy-match"` confirms the URL was exempted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/proxyline",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Process-global proxy routing for Node.js.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,26 +22,37 @@
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
24
24
  "default": "./dist/index.js"
25
+ },
26
+ "./dispatcher-brand": {
27
+ "types": "./dist/dispatcher-brand.d.ts",
28
+ "default": "./dist/dispatcher-brand.js"
25
29
  }
26
30
  },
27
31
  "files": [
28
32
  "dist/*.js",
29
33
  "dist/*.d.ts",
30
34
  "dist/*.d.ts.map",
35
+ "src/**/*.ts",
36
+ "docs/*.md",
37
+ "docs/CNAME",
38
+ "scripts/prepack-build.mjs",
39
+ "tsconfig.json",
40
+ "tsconfig.build.json",
31
41
  "CHANGELOG.md",
32
42
  "README.md",
33
43
  "LICENSE"
34
44
  ],
35
45
  "scripts": {
36
46
  "build": "tsc -p tsconfig.build.json",
37
- "check": "pnpm build && tsc --noEmit && pnpm coverage:run",
47
+ "check": "pnpm build && tsc --noEmit && pnpm coverage:run && tsx --test test/package-artifact.test.ts",
38
48
  "coverage": "pnpm build && pnpm coverage:run",
39
49
  "coverage:run": "node scripts/run-coverage.mjs",
40
50
  "docs:build": "node scripts/build-docs-site.mjs",
41
51
  "package:dry-run": "pnpm pack --dry-run",
42
52
  "prepack": "node scripts/prepack-build.mjs",
53
+ "prepare": "node scripts/prepack-build.mjs",
43
54
  "publish:dry-run": "pnpm publish --dry-run --access public",
44
- "test": "pnpm build && tsx --test test/index.test.ts test/e2e.test.ts test/package.test.ts",
55
+ "test": "pnpm build && tsx --test test/index.test.ts test/e2e.test.ts test/package.test.ts test/scripts.test.ts && tsx --test test/package-artifact.test.ts",
45
56
  "typecheck": "pnpm build && tsc --noEmit"
46
57
  },
47
58
  "devDependencies": {
@@ -49,12 +60,13 @@
49
60
  "@types/ws": "^8.18.1",
50
61
  "tsx": "^4.21.0",
51
62
  "typescript": "^5.9.3",
63
+ "undici": "^7.25.0",
52
64
  "ws": "^8.20.0"
53
65
  },
54
66
  "engines": {
55
67
  "node": ">=20.18.1"
56
68
  },
57
- "dependencies": {
58
- "undici": "^7.25.0"
69
+ "peerDependencies": {
70
+ "undici": ">=7.25.0 <9"
59
71
  }
60
72
  }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ function resolveTypeScriptCompiler() {
8
+ try {
9
+ return require.resolve("typescript/bin/tsc");
10
+ } catch {
11
+ return undefined;
12
+ }
13
+ }
14
+
15
+ function run(command, args, env = {}) {
16
+ const result = spawnSync(command, args, {
17
+ stdio: "inherit",
18
+ env: { ...process.env, ...env },
19
+ });
20
+ if (result.status !== 0) {
21
+ process.exit(result.status ?? 1);
22
+ }
23
+ }
24
+
25
+ const tscBin = resolveTypeScriptCompiler();
26
+
27
+ if (!tscBin) {
28
+ console.error("TypeScript compiler is unavailable. Run `pnpm install --frozen-lockfile` before packing.");
29
+ process.exit(1);
30
+ }
31
+
32
+ run(process.execPath, [tscBin, "-p", "tsconfig.build.json"]);