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..80f2318 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ export type Config = { } upstream: { url: string + proxy?: string } auth: { tokens: TokenEntry[] diff --git a/src/index.ts b/src/index.ts index 990a221..ededa4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { loadConfig } from './config.js' import { setLogLevel, log } from './logger.js' import { initOAuth } from './oauth.js' +import { initVersion } from './version.js' import { startProxy } from './proxy.js' const configPath = process.argv[2] @@ -14,6 +15,9 @@ try { // Initialize OAuth first - gateway manages the token lifecycle await initOAuth(config.oauth.refresh_token) + // Sync version from npm registry (+ hourly auto-refresh) + await initVersion(config) + startProxy(config) } catch (err) { console.error(`Fatal: ${err instanceof Error ? err.message : err}`) diff --git a/src/proxy.ts b/src/proxy.ts index c4ed8a4..6d5dfba 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -185,7 +185,7 @@ function buildVerificationPayload(config: Config) { system: [ { type: 'text', - text: `x-anthropic-billing-header: cc_version=2.1.81.a1b; cc_entrypoint=cli;`, + text: `x-anthropic-billing-header: cc_version=${config.env.version}.a1b; cc_entrypoint=cli;`, }, { type: 'text', diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..feccfe9 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,85 @@ +import { request as httpsRequest } from 'https' +import { HttpsProxyAgent } from 'https-proxy-agent' +import type { Config } from './config.js' +import { log } from './logger.js' + +const REGISTRY_URL = 'https://registry.npmjs.org/@anthropic-ai/claude-code' +const REFRESH_INTERVAL = 60 * 60 * 1000 // 1 hour +const TIMEOUT = 5_000 + +type NpmRegistryInfo = { + 'dist-tags': { latest: string } + time: Record +} + +function fetchLatestVersion(proxyAgent?: HttpsProxyAgent): Promise<{ version: string; buildTime: string }> { + return new Promise((resolve, reject) => { + const url = new URL(REGISTRY_URL) + const req = httpsRequest( + { + hostname: url.hostname, + port: 443, + path: url.pathname, + method: 'GET', + headers: { + 'Accept': 'application/vnd.npm.install-v1+json', + }, + timeout: TIMEOUT, + ...(proxyAgent ? { agent: proxyAgent } : {}), + }, + (res) => { + const chunks: Buffer[] = [] + res.on('data', (chunk) => chunks.push(chunk)) + res.on('end', () => { + try { + const data = JSON.parse(Buffer.concat(chunks).toString('utf-8')) as NpmRegistryInfo + const latest = data['dist-tags']?.latest + if (!latest) { + reject(new Error('No latest version in registry response')) + return + } + const buildTime = data.time?.[latest] || new Date().toISOString() + resolve({ version: latest, buildTime }) + } catch (err) { + reject(new Error(`Failed to parse registry response: ${err}`)) + } + }) + }, + ) + req.on('timeout', () => { + req.destroy() + reject(new Error('Registry request timed out')) + }) + req.on('error', reject) + req.end() + }) +} + +function applyVersion(config: Config, version: string, buildTime: string) { + config.env.version = version + config.env.version_base = version + config.env.build_time = buildTime +} + +async function syncVersion(config: Config, proxyAgent?: HttpsProxyAgent): Promise { + try { + const { version, buildTime } = await fetchLatestVersion(proxyAgent) + const prev = config.env.version + applyVersion(config, version, buildTime) + if (prev !== version) { + log('info', `Version synced: ${prev} -> ${version} (build: ${buildTime})`) + } else { + log('debug', `Version unchanged: ${version}`) + } + } catch (err) { + log('warn', `Version sync failed, keeping ${config.env.version}: ${err}`) + } +} + +export async function initVersion(config: Config): Promise { + const proxyAgent = config.upstream.proxy ? new HttpsProxyAgent(config.upstream.proxy) : undefined + await syncVersion(config, proxyAgent) + + setInterval(() => syncVersion(config, proxyAgent), REFRESH_INTERVAL) + log('info', `Version auto-refresh scheduled every ${REFRESH_INTERVAL / 60000} min`) +}