From d879e0722605e5974c11bbfe0cc511d3fe9ebf70 Mon Sep 17 00:00:00 2001 From: tfq <45652027+0x7551@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:27:55 +0800 Subject: [PATCH] feat: add upstream proxy support for API and OAuth requests Add optional `upstream.proxy` config field to route all outbound requests (API proxy + OAuth token refresh) through an HTTP/SOCKS proxy, useful for deployments behind a corporate firewall or in restricted network environments. Co-Authored-By: Claude Opus 4.6 (1M context) --- config.example.yaml | 1 + package-lock.json | 47 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/config.ts | 1 + src/index.ts | 2 +- src/oauth.ts | 7 ++++++- src/proxy.ts | 7 ++++++- 7 files changed, 63 insertions(+), 3 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 5880815..6df389d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -12,6 +12,7 @@ server: # Upstream Anthropic API upstream: url: https://api.anthropic.com + # proxy: "http://user:pass@host:port" # Optional: upstream proxy for API requests # OAuth - gateway manages token lifecycle centrally # Step 1: On admin machine, run `claude` and do browser OAuth login diff --git a/package-lock.json b/package-lock.json index a6600b8..152e674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "": { "name": "cc-gateway", "version": "0.1.0", + "license": "MIT", "dependencies": { + "https-proxy-agent": "^9.0.0", "yaml": "^2.7.0" }, "devDependencies": { @@ -468,6 +470,32 @@ "undici-types": "~6.21.0" } }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", @@ -538,6 +566,25 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", diff --git a/package.json b/package.json index e92a716..7ab5a41 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "tsx tests/rewriter.test.ts" }, "dependencies": { + "https-proxy-agent": "^9.0.0", "yaml": "^2.7.0" }, "devDependencies": { diff --git a/src/config.ts b/src/config.ts index 34e022b..778476c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ export type Config = { } upstream: { url: string + proxy?: string // e.g. http://user:pass@host:port } auth: { tokens: TokenEntry[] diff --git a/src/index.ts b/src/index.ts index 990a221..4a793ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ try { log('info', 'CC Gateway starting...') // Initialize OAuth first - gateway manages the token lifecycle - await initOAuth(config.oauth.refresh_token) + await initOAuth(config.oauth.refresh_token, config.upstream.proxy) startProxy(config) } catch (err) { diff --git a/src/oauth.ts b/src/oauth.ts index 4578856..1628823 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -1,4 +1,5 @@ import { request as httpsRequest } from 'https' +import { HttpsProxyAgent } from 'https-proxy-agent' import { log } from './logger.js' const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token' @@ -24,7 +25,10 @@ let cachedTokens: OAuthTokens | null = null * The gateway holds the refresh token and manages access token lifecycle. * Client machines never need to contact platform.claude.com. */ -export async function initOAuth(refreshToken: string): Promise { +let proxyAgent: HttpsProxyAgent | undefined + +export async function initOAuth(refreshToken: string, proxyUrl?: string): Promise { + if (proxyUrl) proxyAgent = new HttpsProxyAgent(proxyUrl) log('info', 'Refreshing OAuth token...') cachedTokens = await refreshOAuthToken(refreshToken) log('info', `OAuth token acquired, expires at ${new Date(cachedTokens.expiresAt).toISOString()}`) @@ -87,6 +91,7 @@ function refreshOAuthToken(refreshToken: string): Promise { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(body)), }, + ...(proxyAgent ? { agent: proxyAgent } : {}), }, (res) => { const chunks: Buffer[] = [] diff --git a/src/proxy.ts b/src/proxy.ts index c4ed8a4..b9b1155 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -3,6 +3,7 @@ import { createServer as createHttpServer, type IncomingMessage, type ServerResp import { readFileSync } from 'fs' import { request as httpsRequest } from 'https' import { URL } from 'url' +import { HttpsProxyAgent } from 'https-proxy-agent' import type { Config } from './config.js' import { authenticate, initAuth } from './auth.js' import { getAccessToken } from './oauth.js' @@ -13,10 +14,12 @@ export function startProxy(config: Config) { initAuth(config) const upstream = new URL(config.upstream.url) + const proxyAgent = config.upstream.proxy ? new HttpsProxyAgent(config.upstream.proxy) : undefined + if (proxyAgent) log('info', `Using upstream proxy: ${config.upstream.proxy!.replace(/:([^:@]+)@/, ':***@')}`) const useTls = config.server.tls?.cert && config.server.tls?.key const handler = (req: IncomingMessage, res: ServerResponse) => { - handleRequest(req, res, config, upstream) + handleRequest(req, res, config, upstream, proxyAgent) } let server @@ -46,6 +49,7 @@ async function handleRequest( res: ServerResponse, config: Config, upstream: URL, + proxyAgent?: HttpsProxyAgent, ) { const method = req.method || 'GET' const path = req.url || '/' @@ -135,6 +139,7 @@ async function handleRequest( host: upstream.host, 'content-length': String(body.length), }, + ...(proxyAgent ? { agent: proxyAgent } : {}), }, (proxyRes) => { const status = proxyRes.statusCode || 502