Selaa lähdekoodia

docs(http-proxy): state the shipped library, not the retired plugin

Review found the package's prose still describing a plugin that an
earlier revision removed, and three factual slips about behavior.

- policy.ts, install.ts, install.spec.ts, and the Agent Note named a
  `Config` surface, a `cordis.yml` source, a mountable plugin, and a
  `plugin.spec.ts` that no longer exist; each now describes the
  environment-only resolution the launcher actually runs.
- `NO_PROXY=example.com` bypasses `api.example.com` as well — the matcher
  accepts the host and every subdomain under it, and a leading `.` or
  `*.` means the same thing. The guide, README, and JSDoc claimed a bare
  entry matched only the exact host, which would let a reader believe a
  subdomain was proxied when it went direct.
- A rejection diagnostic names the variable and never its value, so no
  username is shown; the guide said the username was shown with the rest
  masked. The README's source map claimed a "redaction" step that does
  not exist.
- The `node:https` worker placeholder was added for a `node:http` agent
  factory this PR later removed; nothing imports `node:https` now, so the
  stub, its VFS mapping, and its test return to their state on master.
Yichen Jiang 1 kuukausi sitten
vanhempi
sitoutus
2795940323

+ 2 - 2
.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

+ 3 - 3
.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
 

+ 3 - 3
.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
 

+ 2 - 2
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

+ 2 - 2
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.
 

+ 2 - 2
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` 不会有任何效果。请改用主机名或域名后缀。
 

+ 0 - 1
packages/experimental/webworker-runtime/src/module-proxies.ts

@@ -41,7 +41,6 @@ export const MODULE_PROXIES: Record<string, string> = {
   // `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.

+ 0 - 49
packages/experimental/webworker-runtime/src/node/builtin_modules/mock/https.ts

@@ -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<Omit<typeof import('node:https'), 'Agent'>> & Record<'Agent', unknown>
-
-/** CommonJS default export: the members `require()` hands a caller of this module. */
-export default { Agent, request, get, createServer } satisfies NodeFace

+ 0 - 16
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])

+ 2 - 2
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

+ 2 - 2
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.
 
 -----
 

+ 2 - 2
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`,必须改写成后缀形式。
 
 -----
 

+ 5 - 5
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<vo
  * not swapped for the HTTP one this package fell back to for that scheme.
  *
  * A scheme the user named in neither casing carries the resolved value instead of being removed.
- * Without that the child's routing silently diverges from its parent's: `NODE_USE_ENV_PROXY` reads
- * neither `ALL_PROXY` nor a proxy that came from `cordis.yml`, so the child would connect directly
- * while the parent proxies.
+ * Without that the child's routing silently diverges from its parent's: `NODE_USE_ENV_PROXY` does
+ * not read `ALL_PROXY`, so a child of a parent that resolved its proxy from that name would connect
+ * directly while the parent proxies.
  *
  * The bypass list is always the resolved one. It only ever adds the loopback entries to what
  * the user wrote, so nothing is lost, and the child stops sending its own localhost traffic to a

+ 8 - 6
packages/util/http-proxy/src/policy.ts

@@ -1,6 +1,6 @@
 /**
  * Proxy policy resolution: the pure, transport-free half of this package. It turns the launch
- * environment plus optional configuration into one {@link ProxyPolicy}, and answers which proxy
+ * environment into one {@link ProxyPolicy}, and answers which proxy
  * (if any) a given URL goes through.
  *
  * Nothing here imports `undici`, so the module stays loadable in the browser-worker runtime that
@@ -82,7 +82,7 @@ export const DIRECT_POLICY: ProxyPolicy = { noProxy: '', source: 'none' }
 export interface ProxyDiagnostic {
   /** `socks` for a SOCKS or PAC URL this package cannot route; `invalid` for anything unparseable. */
   readonly kind: 'socks' | 'invalid'
-  /** Where the rejected value came from: an environment variable name, or `config.<field>`. */
+  /** 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.

+ 3 - 3
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 }))