# Surfaces

Proxyline installs at the layer above each network API. It does not own sockets, except via the explicit `openProxyConnectTunnel` helper.

## node:http and node:https

Both modules have their request entry points patched and their `globalAgent` replaced.

Patched methods:

- `http.request`, `http.get`
- `https.request`, `https.get`

Per request, Proxyline:

1. Copies the call's options object so caller state is not mutated.
2. Reads TLS-relevant options off any caller-supplied `agent` and lifts them onto the request options (see [TLS identity preservation](#tls-identity-preservation)).
3. Sets `servername` to the destination hostname when not already set and the hostname is not an IP literal.
4. Builds a fresh Proxyline Node agent for the request and assigns it as `options.agent`.
5. Deletes any `createConnection` override so callers cannot punch through to the kernel directly.
6. Destroys the per-request proxy agent when the request closes.

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.

### TLS identity preservation

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:

`ca`, `cert`, `ciphers`, `clientCertEngine`, `crl`, `dhparam`, `ecdhCurve`, `honorCipherOrder`, `key`, `maxVersion`, `minVersion`, `passphrase`, `pfx`, `rejectUnauthorized`, `secureOptions`, `secureProtocol`, `sessionIdContext`.

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.

### Absolute-form requests

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.

## undici and fetch

`installGlobalProxy` calls `undici.setGlobalDispatcher` and patches `globalThis.fetch` to use Proxyline's dispatcher with:

- Proxyline's managed dispatcher in managed mode, backed by `undici.ProxyAgent` instances pointed at `proxyUrl` and trusting `proxyTls` when supplied.
- 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`.

The original dispatcher and fetch globals are captured and restored on `stop()`.
Pass `undici` options to tune Proxyline-owned dispatcher defaults such as `bodyTimeout`, `headersTimeout`, `allowH2`, and `connect.autoSelectFamily`.

```ts
import { fetch } from "undici";

await fetch("https://api.example.com/health"); // routed through Proxyline
```

In managed mode, Proxyline's patched `globalThis.fetch` passes Proxyline's own dispatcher explicitly, so a per-call undici `Agent` or a later `setGlobalDispatcher()` call cannot bypass the managed proxy through that global fetch path. In ambient mode, and for callers using imported `undici.fetch` directly, an explicit undici `Agent` or `Dispatcher` still wins. Use `proxy.createUndiciDispatcher()` to get a dispatcher pre-wired to the same policy.

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.

## WebSocket

WebSocket clients that accept a Node `agent` option (e.g. `ws`) can route through the proxy via:

```ts
import WebSocket from "ws";

const socket = new WebSocket("wss://events.example.com/", {
  agent: proxy.createWebSocketAgent(),
});
```

When ambient mode is inactive, `createWebSocketAgent()` returns a direct-routing agent, so calling code does not need a conditional path.

Clients that do **not** expose an `agent` option but still route their handshake through `http.request` are covered automatically by the global patch.

## HTTP CONNECT tunnel

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).

```ts
import { openProxyConnectTunnel } from "@openclaw/proxyline";

const socket = await openProxyConnectTunnel({
  proxyUrl: "https://proxy.corp.example:8443",
  proxyTls: { caFile: "/etc/proxy-ca.pem" },
  targetHost: "api.example.com",
  targetPort: 443,
  timeoutMs: 2_000,
  signal: AbortSignal.timeout(2_000),
});
```

Properties:

- HTTP and HTTPS proxy endpoints supported. Other schemes throw `UNSUPPORTED_PROXY_PROTOCOL`.
- HTTPS proxies use ALPN `http/1.1`. SNI is the proxy hostname unless that is an IP literal.
- Userinfo in the `proxyUrl` becomes a `Proxy-Authorization: Basic ...` header.
- A bounded `16 KiB` header buffer protects against malicious or runaway proxy responses.
- `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry. When omitted, the default is 30 seconds; pass `0` for no timeout.
- `signal` aborts an in-progress handshake and destroys its active proxy socket.
- Bytes the proxy sends after the response headers are re-injected with `socket.unshift()` so the caller sees the full target stream.
- Non-2xx status lines, header overrun, premature close, and socket errors are all surfaced as `ProxylineError` with code `CONNECT_FAILED`.

The helper is standalone — it does not require `installGlobalProxy` to have been called.

## Caller-built agents

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.

This is intentional: a common bypass is `new https.Agent({ keepAlive: true })` passed per-request. Without replacement the proxy is silently skipped.

To get a proxy-aware agent without going through the patched globals, use:

- `proxy.createNodeAgent()` — an `http.Agent` that proxies HTTP and HTTPS requests.
- `createAmbientNodeProxyAgent()` — a standalone ambient helper that returns an agent only when proxy env applies.
- `proxy.createUndiciDispatcher()` — an undici `Dispatcher` mirroring the current mode.
- `proxy.createWebSocketAgent()` — an `http.Agent` suitable for WebSocket upgrades.

Helper-created agents and dispatchers are caller-owned. Destroy or close them when the caller is done.

When the runtime is inactive (ambient mode with no env proxy set), these helpers return direct agents/dispatchers.
