Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "tsx tests/rewriter.test.ts"
},
"dependencies": {
"https-proxy-agent": "^9.0.0",
"yaml": "^2.7.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Config = {
}
upstream: {
url: string
proxy?: string
}
auth: {
tokens: TokenEntry[]
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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}`)
Expand Down
2 changes: 1 addition & 1 deletion src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
85 changes: 85 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
}

function fetchLatestVersion(proxyAgent?: HttpsProxyAgent<string>): 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<string>): Promise<void> {
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<void> {
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`)
}