From c8f474a07160ead5c343b7795b64d314bdbcec7b Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Wed, 6 May 2026 10:33:19 -0300 Subject: [PATCH 1/2] feat: honor HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars in loggedFetch Bun's fetch() does not auto-read these env vars, so tools like mitmproxy and Charles couldn't intercept CLI traffic. Resolve a proxy per request from curl-style env vars (uppercase or lowercase) and pass it through Bun's `proxy` option. Always skip localhost so the OAuth callback listener is never proxied. Log the chosen proxy under --verbose. --- .changeset/fetch-proxy-env.md | 7 +++++ packages/cli-core/src/lib/fetch.ts | 45 +++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .changeset/fetch-proxy-env.md diff --git a/.changeset/fetch-proxy-env.md b/.changeset/fetch-proxy-env.md new file mode 100644 index 00000000..8e1016e1 --- /dev/null +++ b/.changeset/fetch-proxy-env.md @@ -0,0 +1,7 @@ +--- +"clerk": patch +--- + +Honor `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` environment variables for outbound HTTP requests. + +Bun's `fetch()` does not read these variables automatically, which made tools like mitmproxy and Charles unable to inspect the CLI's traffic. The `loggedFetch` helper now resolves a proxy from the standard env vars (uppercase or lowercase) and passes it through Bun's per-request `proxy` option. Localhost is always skipped so the local OAuth callback listener is never proxied. With `--verbose`, the chosen proxy is logged alongside the request. diff --git a/packages/cli-core/src/lib/fetch.ts b/packages/cli-core/src/lib/fetch.ts index 4de8de20..03348aa9 100644 --- a/packages/cli-core/src/lib/fetch.ts +++ b/packages/cli-core/src/lib/fetch.ts @@ -18,9 +18,12 @@ export async function loggedFetch(url: URL | string, options: LoggedFetchInit): const method = init.method ?? "GET"; const urlStr = url.toString(); log.debug(`${tag}: ${method} ${urlStr}`); + const proxy = resolveProxy(urlStr); + const fetchInit = proxy ? { ...init, proxy } : init; + if (proxy) log.debug(`${tag}: routing via proxy ${proxy}`); const response = await withNetworkAccess( { operation: "connect", target: urlStr, label: tag }, - async () => fetch(url, init), + async () => fetch(url, fetchInit), ); if (!response.ok) { // Clone so the caller can still consume the body for error construction. @@ -29,3 +32,43 @@ export async function loggedFetch(url: URL | string, options: LoggedFetchInit): } return response; } + +/** + * Resolve a proxy URL for the given target by reading curl-style env vars + * (HTTPS_PROXY, HTTP_PROXY, NO_PROXY — uppercase or lowercase). Bun's + * fetch() does not honor these automatically, so we plumb them through the + * `proxy` option on a per-request basis. + * + * Localhost is always skipped so the local OAuth callback listener never + * gets routed through an external proxy. + */ +function resolveProxy(urlStr: string): string | undefined { + let parsed: URL; + try { + parsed = new URL(urlStr); + } catch { + return undefined; + } + const host = parsed.hostname; + if (host === "localhost" || host === "127.0.0.1" || host === "::1") return undefined; + if (matchesNoProxy(host, env("NO_PROXY"))) return undefined; + const proxy = parsed.protocol === "http:" ? env("HTTP_PROXY") : env("HTTPS_PROXY"); + return proxy?.trim() || undefined; +} + +function env(name: string): string | undefined { + return process.env[name] ?? process.env[name.toLowerCase()]; +} + +function matchesNoProxy(host: string, noProxy: string | undefined): boolean { + if (!noProxy) return false; + const normalized = host.toLowerCase(); + for (const raw of noProxy.split(",")) { + const entry = raw.trim().toLowerCase(); + if (!entry) continue; + if (entry === "*") return true; + const suffix = entry.startsWith(".") ? entry : `.${entry}`; + if (normalized === entry || normalized.endsWith(suffix)) return true; + } + return false; +} From 3dae5b7c8b0ccd1704dc8dd30966afc92d8f734b Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Wed, 6 May 2026 17:19:22 -0300 Subject: [PATCH 2/2] fix(fetch): redact proxy credentials from verbose log Proxy env vars can include inline credentials (e.g. http://user:pass@proxy:8080). Strip userinfo before passing the URL to log.debug so credentials are never emitted in --verbose output or captured in CI logs. --- packages/cli-core/src/lib/fetch.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/cli-core/src/lib/fetch.ts b/packages/cli-core/src/lib/fetch.ts index 03348aa9..35ac34fa 100644 --- a/packages/cli-core/src/lib/fetch.ts +++ b/packages/cli-core/src/lib/fetch.ts @@ -20,7 +20,7 @@ export async function loggedFetch(url: URL | string, options: LoggedFetchInit): log.debug(`${tag}: ${method} ${urlStr}`); const proxy = resolveProxy(urlStr); const fetchInit = proxy ? { ...init, proxy } : init; - if (proxy) log.debug(`${tag}: routing via proxy ${proxy}`); + if (proxy) log.debug(`${tag}: routing via proxy ${redactProxy(proxy)}`); const response = await withNetworkAccess( { operation: "connect", target: urlStr, label: tag }, async () => fetch(url, fetchInit), @@ -33,6 +33,20 @@ export async function loggedFetch(url: URL | string, options: LoggedFetchInit): return response; } +function redactProxy(proxy: string): string { + try { + const u = new URL(proxy); + if (u.username || u.password) { + u.username = ""; + u.password = ""; + return u.toString(); + } + return proxy; + } catch { + return proxy; + } +} + /** * Resolve a proxy URL for the given target by reading curl-style env vars * (HTTPS_PROXY, HTTP_PROXY, NO_PROXY — uppercase or lowercase). Bun's