|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const redis = require('../models/redis') |
|
|
const logger = require('../utils/logger') |
|
|
|
|
|
class ClaudeCodeHeadersService { |
|
|
constructor() { |
|
|
this.defaultHeaders = { |
|
|
'x-stainless-retry-count': '0', |
|
|
'x-stainless-timeout': '60', |
|
|
'x-stainless-lang': 'js', |
|
|
'x-stainless-package-version': '0.55.1', |
|
|
'x-stainless-os': 'Windows', |
|
|
'x-stainless-arch': 'x64', |
|
|
'x-stainless-runtime': 'node', |
|
|
'x-stainless-runtime-version': 'v20.19.2', |
|
|
'anthropic-dangerous-direct-browser-access': 'true', |
|
|
'x-app': 'cli', |
|
|
'user-agent': 'claude-cli/1.0.57 (external, cli)', |
|
|
'accept-language': '*', |
|
|
'sec-fetch-mode': 'cors' |
|
|
} |
|
|
|
|
|
|
|
|
this.claudeCodeHeaderKeys = [ |
|
|
'x-stainless-retry-count', |
|
|
'x-stainless-timeout', |
|
|
'x-stainless-lang', |
|
|
'x-stainless-package-version', |
|
|
'x-stainless-os', |
|
|
'x-stainless-arch', |
|
|
'x-stainless-runtime', |
|
|
'x-stainless-runtime-version', |
|
|
'anthropic-dangerous-direct-browser-access', |
|
|
'x-app', |
|
|
'user-agent', |
|
|
'accept-language', |
|
|
'sec-fetch-mode', |
|
|
'accept-encoding' |
|
|
] |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extractVersionFromUserAgent(userAgent) { |
|
|
if (!userAgent) { |
|
|
return null |
|
|
} |
|
|
const match = userAgent.match(/claude-cli\/([\d.]+(?:[a-zA-Z0-9-]*)?)/i) |
|
|
return match ? match[1] : null |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
compareVersions(v1, v2) { |
|
|
if (!v1 || !v2) { |
|
|
return 0 |
|
|
} |
|
|
|
|
|
const parts1 = v1.split('.').map(Number) |
|
|
const parts2 = v2.split('.').map(Number) |
|
|
|
|
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { |
|
|
const p1 = parts1[i] || 0 |
|
|
const p2 = parts2[i] || 0 |
|
|
|
|
|
if (p1 > p2) { |
|
|
return 1 |
|
|
} |
|
|
if (p1 < p2) { |
|
|
return -1 |
|
|
} |
|
|
} |
|
|
|
|
|
return 0 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extractClaudeCodeHeaders(clientHeaders) { |
|
|
const headers = {} |
|
|
|
|
|
|
|
|
const lowerCaseHeaders = {} |
|
|
Object.keys(clientHeaders || {}).forEach((key) => { |
|
|
lowerCaseHeaders[key.toLowerCase()] = clientHeaders[key] |
|
|
}) |
|
|
|
|
|
|
|
|
this.claudeCodeHeaderKeys.forEach((key) => { |
|
|
const lowerKey = key.toLowerCase() |
|
|
if (lowerCaseHeaders[lowerKey]) { |
|
|
headers[key] = lowerCaseHeaders[lowerKey] |
|
|
} |
|
|
}) |
|
|
|
|
|
return headers |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async storeAccountHeaders(accountId, clientHeaders) { |
|
|
try { |
|
|
const extractedHeaders = this.extractClaudeCodeHeaders(clientHeaders) |
|
|
|
|
|
|
|
|
const userAgent = extractedHeaders['user-agent'] |
|
|
if (!userAgent || !/^claude-cli\/[\d.]+\s+\(/i.test(userAgent)) { |
|
|
|
|
|
return |
|
|
} |
|
|
|
|
|
const version = this.extractVersionFromUserAgent(userAgent) |
|
|
if (!version) { |
|
|
logger.warn(`⚠️ Failed to extract version from user-agent: ${userAgent}`) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
const key = `claude_code_headers:${accountId}` |
|
|
const currentData = await redis.getClient().get(key) |
|
|
|
|
|
if (currentData) { |
|
|
const current = JSON.parse(currentData) |
|
|
const currentVersion = this.extractVersionFromUserAgent(current.headers['user-agent']) |
|
|
|
|
|
|
|
|
if (this.compareVersions(version, currentVersion) <= 0) { |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const data = { |
|
|
headers: extractedHeaders, |
|
|
version, |
|
|
updatedAt: new Date().toISOString() |
|
|
} |
|
|
|
|
|
await redis.getClient().setex(key, 86400 * 7, JSON.stringify(data)) |
|
|
|
|
|
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`) |
|
|
} catch (error) { |
|
|
logger.error(`❌ Failed to store Claude Code headers for account ${accountId}:`, error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getAccountHeaders(accountId) { |
|
|
try { |
|
|
const key = `claude_code_headers:${accountId}` |
|
|
const data = await redis.getClient().get(key) |
|
|
|
|
|
if (data) { |
|
|
const parsed = JSON.parse(data) |
|
|
logger.debug( |
|
|
`📋 Retrieved Claude Code headers for account ${accountId}, version: ${parsed.version}` |
|
|
) |
|
|
return parsed.headers |
|
|
} |
|
|
|
|
|
|
|
|
logger.debug(`📋 Using default Claude Code headers for account ${accountId}`) |
|
|
return this.defaultHeaders |
|
|
} catch (error) { |
|
|
logger.error(`❌ Failed to get Claude Code headers for account ${accountId}:`, error) |
|
|
return this.defaultHeaders |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async clearAccountHeaders(accountId) { |
|
|
try { |
|
|
const key = `claude_code_headers:${accountId}` |
|
|
await redis.getClient().del(key) |
|
|
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`) |
|
|
} catch (error) { |
|
|
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getAllAccountHeaders() { |
|
|
try { |
|
|
const pattern = 'claude_code_headers:*' |
|
|
const keys = await redis.getClient().keys(pattern) |
|
|
|
|
|
const results = {} |
|
|
for (const key of keys) { |
|
|
const accountId = key.replace('claude_code_headers:', '') |
|
|
const data = await redis.getClient().get(key) |
|
|
if (data) { |
|
|
results[accountId] = JSON.parse(data) |
|
|
} |
|
|
} |
|
|
|
|
|
return results |
|
|
} catch (error) { |
|
|
logger.error('❌ Failed to get all account headers:', error) |
|
|
return {} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = new ClaudeCodeHeadersService() |
|
|
|