diff --git a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml index d379de0f0f..f8f7fa9937 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md -2026-08-27-outbound-proxy-policy.md: a35b8a909eaa1526d3268e475de4d3f7e093b6eb -2026-08-27-outbound-proxy-policy.zh.md: 278d86faa186c36a289eb477f8b741d46a1dfb0c +2026-08-27-outbound-proxy-policy.md: 0de49f2c3302cfdc611d757828390eb8ef8efe84 +2026-08-27-outbound-proxy-policy.zh.md: 59175834f5257dd3065debffdda97d5fc9645c04 diff --git a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md index a35b8a909e..0de49f2c33 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md +++ b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md @@ -36,7 +36,7 @@ This keeps `proxyForUrl()` and the dispatcher answering from one set of values. **Resolution supplies what neither Node nor undici does.** `ALL_PROXY` backs both schemes; a blank value counts as unset, because undici's `??` chain lets an empty lowercase name shadow a populated uppercase one; loopback is always bypassed, since the Web UI, the Connection transport, and every local test server would otherwise route through the proxy and loop. The bypass list carries `::1` *and* `[::1]`: undici's own matcher reads a bare `::1` as host `:` port `1` and never exempts it. -**Rejection is loud or quiet by where the value came from, and never reroutes the refused scheme.** A slot the user filled and this package refused keeps that scheme direct rather than falling through to `ALL_PROXY` or the HTTP proxy, so the diagnostic and the route agree. A SOCKS URL, an unparseable string, or an unsupported scheme *from the environment* is reported on stderr and skipped — that variable may have been exported for other tools, and a typo in it must not stop the agent from starting. The same value through the plugin's `Config` throws at load, because that is the harness's own configuration surface, where `AGENTS.md` requires misconfiguration to fail loud. +**Rejection is quiet, and never reroutes the refused scheme.** A slot the user filled and this package refused keeps that scheme direct rather than falling through to `ALL_PROXY` or the HTTP proxy, so the diagnostic and the route agree. A SOCKS URL, an unparseable string, or an unsupported scheme is reported on stderr and skipped — the variable may have been exported for other tools, and a typo in it must not stop the agent from starting. The environment is the only source, so no configuration surface exists where `AGENTS.md`'s fail-loud rule would apply instead. **Through a proxy, `web_fetch` stops resolving and pinning.** The provider validates a public address set and pins the connection to it. Through a proxy there is nothing to pin — the proxy performs the origin's DNS — and a pinned direct connection would bypass the proxy entirely. So a proxied hop skips resolution, and configuring a proxy is a statement that the proxy is trusted with destination selection. A hop the policy bypasses, which includes every loopback and every `NO_PROXY` entry, takes the resolved-and-pinned path unchanged. Kimi Code and Claude Code reached this same conclusion independently. @@ -74,7 +74,7 @@ Weighed against that, telemetry is the one outbound channel whose loss costs the ## Consequences -A user who exports `HTTPS_PROXY`, or writes it into a `.env` layer, is proxied everywhere the harness makes a request, with no flag and no configuration. Compositions that want the policy in `cordis.yml` mount the plugin; it is in no shipped bundle, so the default path installs exactly once. +A user who exports `HTTPS_PROXY`, or writes it into a `.env` layer, is proxied everywhere the harness makes a request, with no flag and no configuration. The launcher installs it exactly once, before the first plugin mounts. Because the operating system's settings are not read, the user-facing documentation is now load-bearing rather than supplementary: a user who only toggled "system proxy" in a proxy application gets nothing and no diagnostic. `docs/user/guide/network-proxy.md` therefore states which variables to export and why a browser is proxied when a terminal is not — the three-mechanism confusion is the single most common report, and it is not specific to this harness. @@ -82,7 +82,7 @@ Because the operating system's settings are not read, the user-facing documentat Reaching Node's built-in `fetch` from a userland undici depends on both writing the legacy `Symbol.for('undici.globalDispatcher.1')` slot. That is an implicit cross-version coupling rather than a contract — corepack#834 records it breaking — so `tests/install.spec.ts` drives a real request through a loopback proxy. A version bump that breaks the coupling fails there instead of in the field. -The suite is hermetic against the developer's own environment: `plugin.spec.ts` saves and clears all eight proxy names in both casings. It has to. An exported lowercase `all_proxy` decided a test's outcome during development, because resolution reads lowercase first. +The suite is hermetic against the developer's own environment: every Vitest configuration runs `scripts/test-proxy-environment.ts`, which clears all eight proxy names in both casings before any test, and `install.spec.ts` restores the machine's values around each case that sets its own. It has to. An exported lowercase `all_proxy` decided a test's outcome during development, because resolution reads lowercase first. ## Testing diff --git a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md index 278d86faa1..59175834f5 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md @@ -36,7 +36,7 @@ Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运 **解析补上 Node 与 undici 都不提供的部分。** `ALL_PROXY` 为两种协议兜底;空值视为未设置,因为 undici 的 `??` 链会让空的小写名遮住有值的大写名;loopback 始终绕过,否则 Web UI、Connection 传输以及每一个本地测试服务器都会经由代理并形成回环。绕过列表同时携带 `::1` **与** `[::1]`:undici 自带的匹配器会把裸写的 `::1` 读成主机 `:` 端口 `1`,从而永不豁免它。 -**拒绝是响还是静取决于值从哪来,且绝不为被拒协议改道。** 用户填写而被本包拒绝的槽位,会让该协议保持直连,而不是继续回退到 `ALL_PROXY` 或 HTTP 代理,从而让诊断与实际路由一致。来自**环境**的 SOCKS URL、无法解析的字符串或不受支持的协议,会在 stderr 上报告并跳过——该变量可能是为其他工具导出的,它的笔误不应阻止 agent 启动。同样的值若经由插件的 `Config` 传入,则在加载期抛出,因为那是 Harness 自己的配置面,`AGENTS.md` 要求配置错误必须响。 +**拒绝是静默的,且绝不为被拒协议改道。** 用户填写而被本包拒绝的槽位,会让该协议保持直连,而不是继续回退到 `ALL_PROXY` 或 HTTP 代理,从而让诊断与实际路由一致。SOCKS URL、无法解析的字符串或不受支持的协议,会在 stderr 上报告并跳过——该变量可能是为其他工具导出的,它的笔误不应阻止 agent 启动。环境是唯一来源,因此不存在一个本应适用 `AGENTS.md` 「配置错误必须响」规则的配置面。 **经由代理时,`web_fetch` 不再解析与固定地址。** 该提供方会校验一组公网地址并把连接固定到其上。经由代理时没有可固定的对象——origin 的 DNS 由代理执行——而固定后的直连会彻底绕开代理。因此代理转发的一跳跳过解析,配置代理即表示信任该代理进行目的地选择。被策略绕过的一跳,包括每一个 loopback 与每一条 `NO_PROXY` 条目,仍走原有的解析并固定路径。Kimi Code 与 Claude Code 各自独立得出了同一结论。 @@ -74,7 +74,7 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与 ## Consequences -导出了 `HTTPS_PROXY`、或把它写进 `.env` 层的用户,在 Harness 发起请求的每一处都会走代理,无需任何标志与配置。希望把策略写进 `cordis.yml` 的组合可挂载该插件;它不在任何随附组合包中,因此默认路径只安装一次。 +导出了 `HTTPS_PROXY`、或把它写进 `.env` 层的用户,在 Harness 发起请求的每一处都会走代理,无需任何标志与配置。启动器在第一个插件挂载之前恰好安装一次。 由于不读取操作系统设置,面向用户的文档从补充材料变成了承重件:仅在代理软件里拨了「系统代理」开关的用户什么也得不到,且没有诊断。因此 `docs/user/guide/network-proxy.md` 说明了要导出哪些变量,以及为什么浏览器走代理而终端不走——这个「三套机制」的困惑是最常见的报障,且并非本 Harness 特有。 @@ -82,7 +82,7 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与 userland undici 能触及 Node 内置的 `fetch`,依赖于两者都会写入 legacy 的 `Symbol.for('undici.globalDispatcher.1')` 槽位。那是跨版本的隐式耦合而非约定——corepack#834 记录了它失效的实例——因此 `tests/install.spec.ts` 会驱动一次真实请求穿过 loopback 代理。破坏该耦合的版本升级会在那里失败,而不是流到线上。 -测试套件对开发者自身的环境免疫:`plugin.spec.ts` 会保存并清除全部八个代理变量名的两种大小写形式。这是必需的。开发过程中,一个已导出的小写 `all_proxy` 曾决定了某个测试的结果,因为解析优先读取小写。 +测试套件对开发者自身的环境免疫:每份 Vitest 配置都会先运行 `scripts/test-proxy-environment.ts`,在任何测试之前清除全部八个代理变量名的两种大小写形式;`install.spec.ts` 则在每个自行设值的用例前后还原本机的值。这是必需的。开发过程中,一个已导出的小写 `all_proxy` 曾决定了某个测试的结果,因为解析优先读取小写。 ## Testing diff --git a/docs/user/guide/network-proxy.i18n.yaml b/docs/user/guide/network-proxy.i18n.yaml index f5ed5f8d78..9a942a0b8f 100644 --- a/docs/user/guide/network-proxy.i18n.yaml +++ b/docs/user/guide/network-proxy.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/network-proxy.md -network-proxy.md: 127ee0f2c296d29a9ddd6e8b0f041fca4de4b394 -network-proxy.zh.md: a6efbd32bed7cb07b9e75e71d03b4ca876fc384d +network-proxy.md: 896e4b5416910cf34bcbe5297382f71cf3d3e2bb +network-proxy.zh.md: 8283a5836223b843f51ffd0cc972b803703e1a33 diff --git a/docs/user/guide/network-proxy.md b/docs/user/guide/network-proxy.md index 127ee0f2c2..896e4b5416 100644 --- a/docs/user/guide/network-proxy.md +++ b/docs/user/guide/network-proxy.md @@ -13,7 +13,7 @@ export HTTP_PROXY=http://127.0.0.1:7890 Put both lines in your shell profile so every `dsh` invocation inherits them. DSH also reads a `.env` file in the launch directory and in `$DSH_HOME`, so a proxy that should apply to one project can live there instead; a real environment variable always wins over a file. -A proxy that needs credentials takes them in the URL: `http://user:password@proxy.example:8080`. DSH never prints the password back — a proxy it reports in a diagnostic shows the username and masks the rest. +A proxy that needs credentials takes them in the URL: `http://user:password@proxy.example:8080`. DSH never prints the URL back: a diagnostic names the variable it rejected, so neither the username nor the password appears anywhere. ## Why your browser is proxied but your terminal is not @@ -37,7 +37,7 @@ DSH does not read the operating system's proxy settings. Export the variables, o export NO_PROXY=internal.example.com,.corp.example.com,registry.local ``` -An entry matches an exact host, a `.suffix` or `*.suffix` domain, an optional `:port`, or `*` for everything. +An entry names a host and matches it together with every subdomain under it: `NO_PROXY=example.com` also sends `api.example.com` direct. A leading `.` or `*.` is accepted and means the same thing. An entry may carry a `:port`, and `*` bypasses everything. **CIDR ranges do not work.** An operating system bypass list often contains entries like `10.0.0.0/8` or `192.168.0.0/16`; copying those into `NO_PROXY` has no effect. Use host names or domain suffixes instead. diff --git a/docs/user/guide/network-proxy.zh.md b/docs/user/guide/network-proxy.zh.md index a6efbd32be..8283a58362 100644 --- a/docs/user/guide/network-proxy.zh.md +++ b/docs/user/guide/network-proxy.zh.md @@ -13,7 +13,7 @@ export HTTP_PROXY=http://127.0.0.1:7890 把这两行写进 shell 配置,这样每次调用 `dsh` 都会继承它们。DSH 还会读取启动目录与 `$DSH_HOME` 下的 `.env` 文件,因此只对某个项目生效的代理可以写在那里;真实环境变量始终优先于文件。 -需要凭据的代理把凭据写在 URL 里:`http://user:password@proxy.example:8080`。DSH 绝不会回显密码——诊断信息中出现的代理会显示用户名并掩去其余部分。 +需要凭据的代理把凭据写在 URL 里:`http://user:password@proxy.example:8080`。DSH 绝不会回显这个 URL:诊断只点名被拒绝的变量,因此用户名和密码都不会出现在任何地方。 ## 为什么浏览器走代理、终端却不走 @@ -37,7 +37,7 @@ DSH 不读取操作系统的代理设置。请导出环境变量,或使用 TUN export NO_PROXY=internal.example.com,.corp.example.com,registry.local ``` -一个条目可匹配精确主机、`.suffix` 或 `*.suffix` 域名、可选的 `:port`,或用 `*` 匹配全部。 +一个条目写的是主机名,它连同其下所有子域名一起匹配:`NO_PROXY=example.com` 也会让 `api.example.com` 直连。前缀 `.` 或 `*.` 可以写,含义相同。条目可带 `:port`,`*` 则放行全部。 **CIDR 网段不生效。** 操作系统的绕过列表常含 `10.0.0.0/8` 或 `192.168.0.0/16` 这类条目;把它们复制进 `NO_PROXY` 不会有任何效果。请改用主机名或域名后缀。 diff --git a/packages/experimental/webworker-runtime/src/module-proxies.ts b/packages/experimental/webworker-runtime/src/module-proxies.ts index 3bd28f7430..3bb59ef366 100644 --- a/packages/experimental/webworker-runtime/src/module-proxies.ts +++ b/packages/experimental/webworker-runtime/src/module-proxies.ts @@ -41,7 +41,6 @@ export const MODULE_PROXIES: Record = { // `process` are absent on purpose — the worker host installs that global // (`./globals/process.ts`). 'node:http': './node/builtin_modules/implemented/http.ts', - 'node:https': './node/builtin_modules/mock/https.ts', // Sync-stack AsyncLocalStorage semantics. 'node:async_hooks': './node/builtin_modules/implemented/async_hooks.ts', // Real implementations over browser primitives. diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/mock/https.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/mock/https.ts deleted file mode 100644 index 549c5390fb..0000000000 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/mock/https.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * `node:https` for the worker. Nothing here dials TLS: the only module that reaches for this one is - * `dsh-http-proxy`, whose agent factory serves SDKs that post through Node's core HTTP modules — - * a path the worker never takes, since its own requests go through `fetch`. - */ - -/** Constructible placeholder: an agent built here would have no transport to pool. */ -export class Agent { - /** Teardown is accepted so disposal paths stay quiet. */ - destroy(): void { - // No socket pool was ever held. - } -} - -/** - * TLS requests have no carrier in a worker. - * @returns Never — it throws naming the unavailable member. - */ -export function request(): never { - throw new Error('web-preview: node:https.request is not available in the worker host') -} - -/** - * Counterpart of {@link request} for the GET shorthand. - * @returns Never — it throws naming the unavailable member. - */ -export function get(): never { - throw new Error('web-preview: node:https.get is not available in the worker host') -} - -/** - * TLS listening belongs to the host, not to a worker. - * @returns Never — it throws naming the unavailable member. - */ -export function createServer(): never { - throw new Error('web-preview: node:https.createServer is not available in the worker host') -} - -/** CommonJS interop marker: the worker loader hands `default` to default imports (see ./builtins.ts). */ -export const __esModule = true - -/** - * The `node:https` declarations this module stands in for. `Agent` keeps this module's own class: - * Node declares it over a socket pool that a placeholder holding no connection cannot expose. - */ -type NodeFace = Partial> & Record<'Agent', unknown> - -/** CommonJS default export: the members `require()` hands a caller of this module. */ -export default { Agent, request, get, createServer } satisfies NodeFace diff --git a/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts b/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts index c83f656e32..ea4f1d02a8 100644 --- a/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts @@ -16,7 +16,6 @@ import { notAvailableError, notImplementedFail } from '../../src/node/notImpleme import * as childProcess from '../../src/node/builtin_modules/implemented/child_process.ts' import * as dnsPromises from '../../src/node/builtin_modules/mock/dns/promises.ts' import * as net from '../../src/node/builtin_modules/mock/net.ts' -import * as https from '../../src/node/builtin_modules/mock/https.ts' import * as sqlite from '../../src/node/builtin_modules/mock/sqlite.ts' import * as stream from '../../src/node/builtin_modules/implemented/stream.ts' import * as vm from '../../src/node/builtin_modules/mock/vm.ts' @@ -129,21 +128,6 @@ describe('replaced external packages', () => { }) }) -describe('node:https placeholder', () => { - it('constructs an Agent but refuses every transport member', () => { - const agent = new https.Agent() - // Disposal paths run against agents that never pooled a socket. - expect(() => { agent.destroy() }).not.toThrow() - expect(() => https.request()).toThrow(/https.request is not available/) - expect(() => https.get()).toThrow(/https.get is not available/) - expect(() => https.createServer()).toThrow(/https.createServer is not available/) - }) - - it('exposes the same members through its CommonJS default', () => { - expect(Object.keys(https.default).sort()).toEqual(['Agent', 'createServer', 'get', 'request']) - }) -}) - describe('node:net address predicates', () => { it('classifies IPv4, IPv6, and neither', () => { expect([net.isIPv4('127.0.0.1'), net.isIPv4('255.255.255.255')]).toEqual([true, true]) diff --git a/packages/util/http-proxy/README.i18n.yaml b/packages/util/http-proxy/README.i18n.yaml index 51f6c0f4ab..b5a74ef1a5 100644 --- a/packages/util/http-proxy/README.i18n.yaml +++ b/packages/util/http-proxy/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/http-proxy/README.md -README.md: ffb3b209bbdb19810e1125ec8cd6ce6c01376a8c -README.zh.md: fce5b914a0854ed723ae71c7e779acff70434c9e +README.md: f0dab162eba192ee786e2eed9f19e5739f2d9ad1 +README.zh.md: acdd8200a3f304d9aa63127ba97652d1eadef0f4 diff --git a/packages/util/http-proxy/README.md b/packages/util/http-proxy/README.md index ffb3b209bb..f0dab162eb 100644 --- a/packages/util/http-proxy/README.md +++ b/packages/util/http-proxy/README.md @@ -72,13 +72,13 @@ A proxy value the package cannot use — a SOCKS or PAC URL, an unparseable stri | File | Holds | |---|---| -| `src/policy.ts` | Resolution, bypass matching, and redaction. Imports no transport, so it stays loadable where undici is absent. | +| `src/policy.ts` | Resolution and bypass matching; a diagnostic names the variable, never its value. Imports no transport, so it stays loadable where undici is absent. | | `src/install.ts` | The global dispatcher, the active-policy record, the route, and the child environment. Imports undici dynamically. | | `src/index.ts` | The package face: four functions and one type. | ### Bypass matching -An entry matches an exact host, a `.suffix` or `*.suffix` domain, an optional `:port`, or `*` for everything. A bracketed or bare IPv6 literal matches either way — a bare `::1` is *not* read as host `:` port `1`, which is how undici's own matcher fails and why the resolved list carries both `::1` and `[::1]`. CIDR is not matched: an operating system's bypass list often carries `10.0.0.0/8`, which has to be rewritten as suffixes. +An entry names a host and matches it together with every subdomain under it: `NO_PROXY=example.com` also bypasses `api.example.com`. A leading `.` or `*.` is accepted and means the same thing. An entry may carry a `:port`, and `*` bypasses everything. A bracketed or bare IPv6 literal matches either way — a bare `::1` is *not* read as host `:` port `1`, which is how undici's own matcher fails and why the resolved list carries both `::1` and `[::1]`. CIDR is not matched: an operating system's bypass list often carries `10.0.0.0/8`, which has to be rewritten as suffixes. ----- diff --git a/packages/util/http-proxy/README.zh.md b/packages/util/http-proxy/README.zh.md index fce5b914a0..acdd8200a3 100644 --- a/packages/util/http-proxy/README.zh.md +++ b/packages/util/http-proxy/README.zh.md @@ -72,13 +72,13 @@ loopback 始终被绕过——`localhost`、整个 `127.0.0.0/8` 段、`::1`、` | 文件 | 承载 | |---|---| -| `src/policy.ts` | 解析、绕过匹配与脱敏。不引入任何传输实现,因此在没有 undici 的环境中仍可加载。 | +| `src/policy.ts` | 解析与绕过匹配;诊断只点名变量,从不带出它的值。不引入任何传输实现,因此在没有 undici 的环境中仍可加载。 | | `src/install.ts` | 全局 dispatcher、生效策略记录、路由与子进程环境。动态引入 undici。 | | `src/index.ts` | 本包的对外面:四个函数与一个类型。 | ### 绕过匹配 -一个条目可匹配精确主机、`.suffix` 或 `*.suffix` 域名、可选的 `:port`,或用 `*` 匹配全部。带方括号与裸写的 IPv6 字面量都能匹配——裸写的 `::1` **不会**被读成主机 `:` 端口 `1`,而 undici 自带的匹配器正是这样出错的,这也是解析结果中同时携带 `::1` 与 `[::1]` 的原因。CIDR 不参与匹配:操作系统的绕过列表常含 `10.0.0.0/8`,必须改写成后缀形式。 +一个条目写的是主机名,它连同其下所有子域名一起匹配:`NO_PROXY=example.com` 也会放行 `api.example.com`。前缀 `.` 或 `*.` 可以写,含义相同。条目可带 `:port`,`*` 则放行全部。带方括号与裸写的 IPv6 字面量都能匹配——裸写的 `::1` **不会**被读成主机 `:` 端口 `1`,而 undici 自带的匹配器正是这样出错的,这也是解析结果中同时携带 `::1` 与 `[::1]` 的原因。CIDR 不参与匹配:操作系统的绕过列表常含 `10.0.0.0/8`,必须改写成后缀形式。 ----- diff --git a/packages/util/http-proxy/src/install.ts b/packages/util/http-proxy/src/install.ts index d7035a12f7..1b1bb79326 100644 --- a/packages/util/http-proxy/src/install.ts +++ b/packages/util/http-proxy/src/install.ts @@ -24,8 +24,8 @@ let active: ProxyPolicy | undefined /** * The proxy environment as the user exported it, or `undefined` when no policy is installed. * - * Owned by the OUTERMOST install: a nested one — the plugin mounted over the launcher's policy — - * would otherwise record the outer policy's published values as if the user had written them, and + * Owned by the OUTERMOST install: one layered over the launcher's would otherwise record the outer + * policy's published values as if the user had written them, and * hand every child a normalization the user never asked for. * * {@link proxyEnvironmentForChild} keeps a value the user set rather than the one this process resolved from @@ -201,9 +201,9 @@ async function installGlobalProxy(policy: ProxyPolicy): Promise<() => Promise`. */ + /** The environment variable that supplied the rejected value. */ readonly origin: string /** Operator-facing sentence naming the rejection and the way forward. Carries no credential. */ readonly message: string @@ -118,7 +118,7 @@ function readEnv( } /** - * What one environment or configuration slot supplied. A rejected slot is distinct from an absent + * What one environment variable supplied. A rejected slot is distinct from an absent * one: the user named a proxy for that scheme, so falling back to another scheme's proxy would route * the request somewhere they never asked for while the diagnostic said it stayed direct. */ @@ -188,7 +188,7 @@ function resolveScheme(own: ProxyCandidate, ...fallbacks: (string | undefined)[] * Merge {@link LOOPBACK_NO_PROXY} into a bypass list, preserving the caller's entries and order. * A list of `*` already bypasses everything and is returned unchanged. * - * @param noProxy - the bypass list as the environment or configuration supplied it. + * @param noProxy - the bypass list as the environment supplied it. * @returns the effective bypass list. */ function withLoopback(noProxy: string | undefined): string { @@ -254,8 +254,10 @@ export function isLoopbackHost(hostname: string): boolean { } /** - * Decide whether a bypass list exempts one URL. Entries match an exact host, a `.suffix` or - * `*.suffix` domain, an optional `:port`, or `*` for everything. CIDR notation is not matched — + * Decide whether a bypass list exempts one URL. An entry names a host and matches it together with + * every subdomain under it — `example.com` also bypasses `api.example.com` — and a leading `.` or + * `*.` is accepted as the same thing; an entry may carry a `:port`, and `*` bypasses everything. + * CIDR notation is not matched — * an operating system's bypass list often carries `10.0.0.0/8`, which must be rewritten as suffixes. * * @param noProxy - the effective bypass list. diff --git a/packages/util/http-proxy/tests/install.spec.ts b/packages/util/http-proxy/tests/install.spec.ts index 4b02ea1ee8..52e8011e6f 100644 --- a/packages/util/http-proxy/tests/install.spec.ts +++ b/packages/util/http-proxy/tests/install.spec.ts @@ -215,8 +215,8 @@ describe('proxyRouteFor', () => { // very agent the branch described, so no second read can put the two on different routes. expect(route.dispatcher).toBe(getGlobalDispatcher()) const undici = await import('undici') - // Unmounting the plugin under an in-flight request is what a hot reload does. The shared - // dispatcher is closed, not destroyed, so the hop that already left finishes. + // Disposing the install while a request is in flight: the shared dispatcher is closed, not + // destroyed, so the hop that already left finishes. const inFlight = undici.fetch(proxyTarget, { dispatcher: route.dispatcher }) await dispose() await expect((await inFlight).text()).resolves.toBe('VIA-PROXY') @@ -307,7 +307,7 @@ describe('proxyEnvironmentForChild', () => { await withCleanProxyEnv(async () => { // The user exported one name, in one casing. process.env.HTTP_PROXY = proxyUrl - // The launcher installs first; mounting the plugin installs a second policy over it. + // The launcher installs first; a second `installProxyFromEnvironment` layers another policy over it. const outer = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: proxyUrl, NO_PROXY: 'example.com' })) try { const inner = await install(env({ HTTP_PROXY: nestedUrl, HTTPS_PROXY: nestedUrl }))