chore(deps): update https-proxy-agent to 9.1.0 (#2383)

* chore(deps): update https-proxy-agent to 9.1.0

* test(proxy): isolate CONNECT test from leaked axios mocks

* test(proxy): avoid global axios module registration

* test(proxy): exercise createAxiosInstance in CONNECT test
This commit is contained in:
JATMN
2026-10-24 04:58:01 -07:00
committed by GitHub
parent 196c5981a8
commit 58d8a9d67f
4 changed files with 239 additions and 4 deletions
+28
View File
@@ -249,6 +249,34 @@ content: serialized headers may quote or escape filenames, and parsed patches
can omit filenames. Check the diff and edit-snippet tests when changing these
paths.
### Proxy agents
Use the shared helpers in `src/utils/proxy.ts`: `createAxiosInstance()` for
Axios, `getWebSocketProxyAgent(url)` for Node WebSockets, and `getProxyFetchOptions()`
for fetch. The Axios and WebSocket helpers apply `NO_PROXY` bypasses before
selecting an agent. `getWebSocketProxyUrl(url)` supplies Bun's native WebSocket proxy
option. Lowercase proxy environment variables take precedence over uppercase
variants on the Axios and WebSocket paths.
`https-proxy-agent` is pinned exactly to `9.1.0` and remains an external runtime
module in `scripts/externals.ts`. Use its public named ESM exports, such as
`import { HttpsProxyAgent } from 'https-proxy-agent'`, and the constructor
`new HttpsProxyAgent(proxyUrl, options)`. Both it and `agent-base` are ESM
packages requiring Node.js 20 or later; use OpenClaude's supported Node.js
`>=24.22.1` runtime. Keep dependency updates exact and generate `bun.lock` with
`bun add --exact https-proxy-agent@<version>`.
Keep TLS verification enabled. Configure `NODE_EXTRA_CA_CERTS` before starting
the CLI when a custom CA is needed. For runtime certificate configuration, the
shared CA loader reads that file and the TLS helpers supply explicit options;
clear the CA and mTLS caches after configuration changes. Proxy-agent
constructor options configure the connection to the proxy. Supply target TLS
options on the HTTPS request, using `getWebSocketTLSOptions()` where applicable;
do not assume constructor `ca` options also trust the target inside a CONNECT
tunnel. Run `bun test src/utils/proxy.connect.test.ts src/utils/proxy.test.ts`
when changing these paths to verify HTTPS CONNECT routing and `NO_PROXY`
bypass with certificate validation enabled.
## Provider Changes
OpenClaude supports multiple provider paths. Before contributing provider changes, review the relevant documentation to ensure your implementation follows the expected patterns:
+15 -3
View File
@@ -54,7 +54,7 @@
"google-auth-library": "10.6.2",
"graphology": "0.26.0",
"graphology-metrics": "2.4.2",
"https-proxy-agent": "7.0.6",
"https-proxy-agent": "9.1.0",
"ignore": "7.0.13",
"indent-string": "5.0.0",
"js-tiktoken": "1.0.21",
@@ -597,7 +597,7 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agent-base": ["agent-base@9.0.0", "", {}, "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
@@ -877,7 +877,7 @@
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"https-proxy-agent": ["https-proxy-agent@9.1.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "proxy-agent-negotiate": "1.1.0" } }, "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA=="],
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
@@ -1089,6 +1089,8 @@
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-agent-negotiate": ["proxy-agent-negotiate@1.1.0", "", { "peerDependencies": { "kerberos": "^2.0.0" }, "optionalPeers": ["kerberos"] }, "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -1487,6 +1489,8 @@
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
@@ -1513,8 +1517,12 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"knip/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
"needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -1561,6 +1569,8 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
"@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -1589,6 +1599,8 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
+1 -1
View File
@@ -134,7 +134,7 @@
"google-auth-library": "10.6.2",
"graphology": "0.26.0",
"graphology-metrics": "2.4.2",
"https-proxy-agent": "7.0.6",
"https-proxy-agent": "9.1.0",
"ignore": "7.0.13",
"indent-string": "5.0.0",
"js-tiktoken": "1.0.21",
+195
View File
@@ -0,0 +1,195 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { once } from 'node:events'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { createServer as createHttpServer, type IncomingMessage } from 'node:http'
import {
createServer as createHttpsServer,
get,
request,
type RequestOptions,
} from 'node:https'
import { connect, type AddressInfo } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Duplex } from 'node:stream'
import { snapshotEnv } from '../test/envSnapshot.js'
import { clearCACertsCache } from './caCerts.js'
import { clearMTLSCache, getMTLSAgent, getWebSocketTLSOptions } from './mtls.js'
// Cache-busting proxy.ts alone still imports any leaked global Axios mock.
// Build a fresh copy of the production module with only its Axios import
// redirected to the real CommonJS entry. Keep the other imports external so
// CA/mTLS caches stay shared, without changing any global mock registration.
const nonce = `${Date.now()}-${Math.random()}`
const proxyBuild = await Bun.build({
entrypoints: [join(import.meta.dir, 'proxy.ts')],
target: 'bun',
plugins: [{
name: 'connect-test-real-axios',
setup(build) {
build.onResolve({ filter: /.*/ }, args => {
if (!args.importer) return
return {
path: args.path === 'axios'
? `${require.resolve('axios')}?real=${nonce}`
: Bun.resolveSync(args.path, args.resolveDir),
external: true,
}
})
},
}],
})
if (!proxyBuild.success) {
throw new AggregateError(proxyBuild.logs, 'Could not build proxy.ts for CONNECT tests')
}
const { createAxiosInstance, getWebSocketProxyAgent } = await import(
`data:text/javascript;base64,${Buffer.from(await proxyBuild.outputs[0].text()).toString('base64')}`
) as typeof import('./proxy.js')
// Self-signed test certificate and key, valid only for the loopback target.
const cert = `-----BEGIN CERTIFICATE-----
MIIBmzCCAUGgAwIBAgIUAtOYg4xV7C9catkY/bwZc7XywMgwCgYIKoZIzj0EAwIw
FDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyNDA4NTUxNloYDzIxMjYwOTMw
MDg1NTE2WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O7bSu0aDJpwdMzOxgOT2jTBmK
2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKWo28wbTAdBgNVHQ4EFgQUu+xqnGfB/gu8
DbNNHlVonV69Y64wHwYDVR0jBBgwFoAUu+xqnGfB/gu8DbNNHlVonV69Y64wDwYD
VR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI
zj0EAwIDSAAwRQIgPy5jbT1ECH7LFy5r5JrV7xfsOkhwtbuwqVeidDBynFACIQCp
sTyTz0llkGsTStOnpDBTzTUQzcDT8z6ZgqqxbY29nw==
-----END CERTIFICATE-----`
const key = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXc/iw7bBMrZkWHsr
tDvMDJ6qPLsspu6IH8lHZ3ccGvihRANCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O
7bSu0aDJpwdMzOxgOT2jTBmK2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKW
-----END PRIVATE KEY-----`
describe('HTTPS_PROXY CONNECT transport', () => {
let restoreEnv: () => void
let directory: string
const sockets = new Set<Duplex>()
let target: ReturnType<typeof createHttpsServer>
let proxy: ReturnType<typeof createHttpServer>
let targetUrl: string
let connects: string[]
beforeEach(async () => {
restoreEnv = snapshotEnv([
'HTTPS_PROXY',
'https_proxy',
'HTTP_PROXY',
'http_proxy',
'NO_PROXY',
'no_proxy',
'NODE_EXTRA_CA_CERTS',
'NODE_OPTIONS',
'NODE_TLS_REJECT_UNAUTHORIZED',
'CLAUDE_CODE_CLIENT_CERT',
'CLAUDE_CODE_CLIENT_KEY',
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
'CLAUDE_CODE_PROXY_RESOLVES_HOSTS',
], { clear: true })
directory = mkdtempSync(join(tmpdir(), 'proxy-connect-'))
const caPath = join(directory, 'ca.pem')
writeFileSync(caPath, cert)
process.env.NODE_EXTRA_CA_CERTS = caPath
clearCACertsCache()
clearMTLSCache()
connects = []
target = createHttpsServer({ key, cert }, (_request, response) => {
response.end('secure response')
})
target.listen(0, '127.0.0.1')
await once(target, 'listening')
const targetPort = (target.address() as AddressInfo).port
targetUrl = `https://127.0.0.1:${targetPort}/test`
proxy = createHttpServer()
proxy.on('connect', (request, client, head) => {
connects.push(request.url!)
// Forward only to this test's target; assert the CONNECT authority below.
const upstream = connect(targetPort, '127.0.0.1', () => {
client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
if (head.length) upstream.write(head)
client.pipe(upstream)
upstream.pipe(client)
})
sockets.add(client)
sockets.add(upstream)
client.on('error', () => upstream.destroy())
upstream.on('error', () => client.destroy())
})
proxy.listen(0, '127.0.0.1')
await once(proxy, 'listening')
process.env.HTTPS_PROXY = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`
})
afterEach(async () => {
getMTLSAgent()?.destroy()
for (const socket of sockets) socket.destroy()
sockets.clear()
target?.closeAllConnections()
await Promise.all(
[target, proxy].filter(Boolean).map(server =>
new Promise<void>((resolve, reject) => {
server.close(error => error ? reject(error) : resolve())
}),
),
)
restoreEnv()
clearCACertsCache()
clearMTLSCache()
rmSync(directory, { recursive: true, force: true })
})
test('Axios tunnels HTTPS through HTTPS_PROXY and bypasses it for NO_PROXY', async () => {
const client = createAxiosInstance()
// Native TLS reads NODE_EXTRA_CA_CERTS only at startup. Pass the runtime
// CA loader's result to the target request; agent options configure the proxy.
client.defaults.transport = {
request: (
options: RequestOptions,
callback: (response: IncomingMessage) => void,
) => request({ ...options, ...getWebSocketTLSOptions() }, callback),
}
const response = await client.get(targetUrl, { timeout: 3000 })
expect(response.data).toBe('secure response')
expect(connects).toEqual([new URL(targetUrl).host])
process.env.NO_PROXY = '127.0.0.1'
const direct = await client.get(targetUrl, { timeout: 3000 })
expect(direct.data).toBe('secure response')
expect(connects).toHaveLength(1)
})
test('WebSocket agent tunnels HTTPS and is omitted for NO_PROXY', async () => {
const agent = getWebSocketProxyAgent(targetUrl.replace('https:', 'wss:'))
expect(agent).toBeDefined()
try {
const body = await new Promise<string>((resolve, reject) => {
const request = get(
targetUrl,
{ agent, ...getWebSocketTLSOptions() },
response => {
response.setEncoding('utf8')
let data = ''
response.on('data', chunk => { data += chunk })
response.on('end', () => resolve(data))
response.on('error', reject)
},
)
request.on('error', reject)
request.setTimeout(3000, () =>
request.destroy(new Error('HTTPS request timed out')),
)
})
expect(body).toBe('secure response')
expect(connects).toEqual([new URL(targetUrl).host])
process.env.NO_PROXY = '127.0.0.1'
expect(getWebSocketProxyAgent(targetUrl.replace('https:', 'wss:'))).toBeUndefined()
} finally {
agent?.destroy()
}
})
})