|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const crypto = require('crypto') |
|
|
const ProxyHelper = require('./proxyHelper') |
|
|
const axios = require('axios') |
|
|
const logger = require('./logger') |
|
|
|
|
|
|
|
|
const OAUTH_CONFIG = { |
|
|
AUTHORIZE_URL: 'https://claude.ai/oauth/authorize', |
|
|
TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token', |
|
|
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e', |
|
|
REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback', |
|
|
SCOPES: 'org:create_api_key user:profile user:inference', |
|
|
SCOPES_SETUP: 'user:inference' |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateState() { |
|
|
return crypto.randomBytes(32).toString('base64url') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateCodeVerifier() { |
|
|
return crypto.randomBytes(32).toString('base64url') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateCodeChallenge(codeVerifier) { |
|
|
return crypto.createHash('sha256').update(codeVerifier).digest('base64url') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateAuthUrl(codeChallenge, state) { |
|
|
const params = new URLSearchParams({ |
|
|
code: 'true', |
|
|
client_id: OAUTH_CONFIG.CLIENT_ID, |
|
|
response_type: 'code', |
|
|
redirect_uri: OAUTH_CONFIG.REDIRECT_URI, |
|
|
scope: OAUTH_CONFIG.SCOPES, |
|
|
code_challenge: codeChallenge, |
|
|
code_challenge_method: 'S256', |
|
|
state |
|
|
}) |
|
|
|
|
|
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}` |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateOAuthParams() { |
|
|
const state = generateState() |
|
|
const codeVerifier = generateCodeVerifier() |
|
|
const codeChallenge = generateCodeChallenge(codeVerifier) |
|
|
|
|
|
const authUrl = generateAuthUrl(codeChallenge, state) |
|
|
|
|
|
return { |
|
|
authUrl, |
|
|
codeVerifier, |
|
|
state, |
|
|
codeChallenge |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateSetupTokenAuthUrl(codeChallenge, state) { |
|
|
const params = new URLSearchParams({ |
|
|
code: 'true', |
|
|
client_id: OAUTH_CONFIG.CLIENT_ID, |
|
|
response_type: 'code', |
|
|
redirect_uri: OAUTH_CONFIG.REDIRECT_URI, |
|
|
scope: OAUTH_CONFIG.SCOPES_SETUP, |
|
|
code_challenge: codeChallenge, |
|
|
code_challenge_method: 'S256', |
|
|
state |
|
|
}) |
|
|
|
|
|
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}` |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateSetupTokenParams() { |
|
|
const state = generateState() |
|
|
const codeVerifier = generateCodeVerifier() |
|
|
const codeChallenge = generateCodeChallenge(codeVerifier) |
|
|
|
|
|
const authUrl = generateSetupTokenAuthUrl(codeChallenge, state) |
|
|
|
|
|
return { |
|
|
authUrl, |
|
|
codeVerifier, |
|
|
state, |
|
|
codeChallenge |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createProxyAgent(proxyConfig) { |
|
|
return ProxyHelper.createProxyAgent(proxyConfig) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, proxyConfig = null) { |
|
|
|
|
|
const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode |
|
|
|
|
|
const params = { |
|
|
grant_type: 'authorization_code', |
|
|
client_id: OAUTH_CONFIG.CLIENT_ID, |
|
|
code: cleanedCode, |
|
|
redirect_uri: OAUTH_CONFIG.REDIRECT_URI, |
|
|
code_verifier: codeVerifier, |
|
|
state |
|
|
} |
|
|
|
|
|
|
|
|
const agent = createProxyAgent(proxyConfig) |
|
|
|
|
|
try { |
|
|
if (agent) { |
|
|
logger.info( |
|
|
`🌐 Using proxy for OAuth token exchange: ${ProxyHelper.maskProxyInfo(proxyConfig)}` |
|
|
) |
|
|
} else { |
|
|
logger.debug('🌐 No proxy configured for OAuth token exchange') |
|
|
} |
|
|
|
|
|
logger.debug('🔄 Attempting OAuth token exchange', { |
|
|
url: OAUTH_CONFIG.TOKEN_URL, |
|
|
codeLength: cleanedCode.length, |
|
|
codePrefix: `${cleanedCode.substring(0, 10)}...`, |
|
|
hasProxy: !!proxyConfig, |
|
|
proxyType: proxyConfig?.type || 'none' |
|
|
}) |
|
|
|
|
|
const axiosConfig = { |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
'User-Agent': 'claude-cli/1.0.56 (external, cli)', |
|
|
Accept: 'application/json, text/plain, */*', |
|
|
'Accept-Language': 'en-US,en;q=0.9', |
|
|
Referer: 'https://claude.ai/', |
|
|
Origin: 'https://claude.ai' |
|
|
}, |
|
|
timeout: 30000 |
|
|
} |
|
|
|
|
|
if (agent) { |
|
|
axiosConfig.httpAgent = agent |
|
|
axiosConfig.httpsAgent = agent |
|
|
axiosConfig.proxy = false |
|
|
} |
|
|
|
|
|
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, axiosConfig) |
|
|
|
|
|
|
|
|
logger.authDetail('OAuth token exchange response', response.data) |
|
|
|
|
|
|
|
|
logger.info('📊 OAuth token exchange response (analyzing for subscription info):', { |
|
|
status: response.status, |
|
|
hasData: !!response.data, |
|
|
dataKeys: response.data ? Object.keys(response.data) : [] |
|
|
}) |
|
|
|
|
|
logger.success('✅ OAuth token exchange successful', { |
|
|
status: response.status, |
|
|
hasAccessToken: !!response.data?.access_token, |
|
|
hasRefreshToken: !!response.data?.refresh_token, |
|
|
scopes: response.data?.scope, |
|
|
|
|
|
subscription: response.data?.subscription, |
|
|
plan: response.data?.plan, |
|
|
tier: response.data?.tier, |
|
|
accountType: response.data?.account_type, |
|
|
features: response.data?.features, |
|
|
limits: response.data?.limits |
|
|
}) |
|
|
|
|
|
const { data } = response |
|
|
|
|
|
|
|
|
const result = { |
|
|
accessToken: data.access_token, |
|
|
refreshToken: data.refresh_token, |
|
|
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000, |
|
|
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'], |
|
|
isMax: true |
|
|
} |
|
|
|
|
|
|
|
|
if (data.subscription || data.plan || data.tier || data.account_type) { |
|
|
result.subscriptionInfo = { |
|
|
subscription: data.subscription, |
|
|
plan: data.plan, |
|
|
tier: data.tier, |
|
|
accountType: data.account_type, |
|
|
features: data.features, |
|
|
limits: data.limits |
|
|
} |
|
|
logger.info('🎯 Found subscription info in OAuth response:', result.subscriptionInfo) |
|
|
} |
|
|
|
|
|
return result |
|
|
} catch (error) { |
|
|
|
|
|
if (error.response) { |
|
|
|
|
|
const { status } = error.response |
|
|
const errorData = error.response.data |
|
|
|
|
|
logger.error('❌ OAuth token exchange failed with server error', { |
|
|
status, |
|
|
statusText: error.response.statusText, |
|
|
headers: error.response.headers, |
|
|
data: errorData, |
|
|
codeLength: cleanedCode.length, |
|
|
codePrefix: `${cleanedCode.substring(0, 10)}...` |
|
|
}) |
|
|
|
|
|
|
|
|
let errorMessage = `HTTP ${status}` |
|
|
|
|
|
if (errorData) { |
|
|
if (typeof errorData === 'string') { |
|
|
errorMessage += `: ${errorData}` |
|
|
} else if (errorData.error) { |
|
|
errorMessage += `: ${errorData.error}` |
|
|
if (errorData.error_description) { |
|
|
errorMessage += ` - ${errorData.error_description}` |
|
|
} |
|
|
} else { |
|
|
errorMessage += `: ${JSON.stringify(errorData)}` |
|
|
} |
|
|
} |
|
|
|
|
|
throw new Error(`Token exchange failed: ${errorMessage}`) |
|
|
} else if (error.request) { |
|
|
|
|
|
logger.error('❌ OAuth token exchange failed with network error', { |
|
|
message: error.message, |
|
|
code: error.code, |
|
|
hasProxy: !!proxyConfig |
|
|
}) |
|
|
throw new Error('Token exchange failed: No response from server (network error or timeout)') |
|
|
} else { |
|
|
|
|
|
logger.error('❌ OAuth token exchange failed with unknown error', { |
|
|
message: error.message, |
|
|
stack: error.stack |
|
|
}) |
|
|
throw new Error(`Token exchange failed: ${error.message}`) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseCallbackUrl(input) { |
|
|
if (!input || typeof input !== 'string') { |
|
|
throw new Error('请提供有效的授权码或回调 URL') |
|
|
} |
|
|
|
|
|
const trimmedInput = input.trim() |
|
|
|
|
|
|
|
|
if (trimmedInput.startsWith('http://') || trimmedInput.startsWith('https://')) { |
|
|
try { |
|
|
const urlObj = new URL(trimmedInput) |
|
|
const authorizationCode = urlObj.searchParams.get('code') |
|
|
|
|
|
if (!authorizationCode) { |
|
|
throw new Error('回调 URL 中未找到授权码 (code 参数)') |
|
|
} |
|
|
|
|
|
return authorizationCode |
|
|
} catch (error) { |
|
|
if (error.message.includes('回调 URL 中未找到授权码')) { |
|
|
throw error |
|
|
} |
|
|
throw new Error('无效的 URL 格式,请检查回调 URL 是否正确') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const cleanedCode = trimmedInput.split('#')[0]?.split('&')[0] ?? trimmedInput |
|
|
|
|
|
|
|
|
if (!cleanedCode || cleanedCode.length < 10) { |
|
|
throw new Error('授权码格式无效,请确保复制了完整的 Authorization Code') |
|
|
} |
|
|
|
|
|
|
|
|
const validCodePattern = /^[A-Za-z0-9_-]+$/ |
|
|
if (!validCodePattern.test(cleanedCode)) { |
|
|
throw new Error('授权码包含无效字符,请检查是否复制了正确的 Authorization Code') |
|
|
} |
|
|
|
|
|
return cleanedCode |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function exchangeSetupTokenCode(authorizationCode, codeVerifier, state, proxyConfig = null) { |
|
|
|
|
|
const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode |
|
|
|
|
|
const params = { |
|
|
grant_type: 'authorization_code', |
|
|
client_id: OAUTH_CONFIG.CLIENT_ID, |
|
|
code: cleanedCode, |
|
|
redirect_uri: OAUTH_CONFIG.REDIRECT_URI, |
|
|
code_verifier: codeVerifier, |
|
|
state, |
|
|
expires_in: 31536000 |
|
|
} |
|
|
|
|
|
|
|
|
const agent = createProxyAgent(proxyConfig) |
|
|
|
|
|
try { |
|
|
if (agent) { |
|
|
logger.info( |
|
|
`🌐 Using proxy for Setup Token exchange: ${ProxyHelper.maskProxyInfo(proxyConfig)}` |
|
|
) |
|
|
} else { |
|
|
logger.debug('🌐 No proxy configured for Setup Token exchange') |
|
|
} |
|
|
|
|
|
logger.debug('🔄 Attempting Setup Token exchange', { |
|
|
url: OAUTH_CONFIG.TOKEN_URL, |
|
|
codeLength: cleanedCode.length, |
|
|
codePrefix: `${cleanedCode.substring(0, 10)}...`, |
|
|
hasProxy: !!proxyConfig, |
|
|
proxyType: proxyConfig?.type || 'none' |
|
|
}) |
|
|
|
|
|
const axiosConfig = { |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
'User-Agent': 'claude-cli/1.0.56 (external, cli)', |
|
|
Accept: 'application/json, text/plain, */*', |
|
|
'Accept-Language': 'en-US,en;q=0.9', |
|
|
Referer: 'https://claude.ai/', |
|
|
Origin: 'https://claude.ai' |
|
|
}, |
|
|
timeout: 30000 |
|
|
} |
|
|
|
|
|
if (agent) { |
|
|
axiosConfig.httpAgent = agent |
|
|
axiosConfig.httpsAgent = agent |
|
|
axiosConfig.proxy = false |
|
|
} |
|
|
|
|
|
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, axiosConfig) |
|
|
|
|
|
|
|
|
logger.authDetail('Setup Token exchange response', response.data) |
|
|
|
|
|
|
|
|
logger.info('📊 Setup Token exchange response (analyzing for subscription info):', { |
|
|
status: response.status, |
|
|
hasData: !!response.data, |
|
|
dataKeys: response.data ? Object.keys(response.data) : [] |
|
|
}) |
|
|
|
|
|
logger.success('✅ Setup Token exchange successful', { |
|
|
status: response.status, |
|
|
hasAccessToken: !!response.data?.access_token, |
|
|
scopes: response.data?.scope, |
|
|
|
|
|
subscription: response.data?.subscription, |
|
|
plan: response.data?.plan, |
|
|
tier: response.data?.tier, |
|
|
accountType: response.data?.account_type, |
|
|
features: response.data?.features, |
|
|
limits: response.data?.limits |
|
|
}) |
|
|
|
|
|
const { data } = response |
|
|
|
|
|
|
|
|
const result = { |
|
|
accessToken: data.access_token, |
|
|
refreshToken: '', |
|
|
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000, |
|
|
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'], |
|
|
isMax: true |
|
|
} |
|
|
|
|
|
|
|
|
if (data.subscription || data.plan || data.tier || data.account_type) { |
|
|
result.subscriptionInfo = { |
|
|
subscription: data.subscription, |
|
|
plan: data.plan, |
|
|
tier: data.tier, |
|
|
accountType: data.account_type, |
|
|
features: data.features, |
|
|
limits: data.limits |
|
|
} |
|
|
logger.info('🎯 Found subscription info in Setup Token response:', result.subscriptionInfo) |
|
|
} |
|
|
|
|
|
return result |
|
|
} catch (error) { |
|
|
|
|
|
if (error.response) { |
|
|
const { status } = error.response |
|
|
const errorData = error.response.data |
|
|
|
|
|
logger.error('❌ Setup Token exchange failed with server error', { |
|
|
status, |
|
|
statusText: error.response.statusText, |
|
|
data: errorData, |
|
|
codeLength: cleanedCode.length, |
|
|
codePrefix: `${cleanedCode.substring(0, 10)}...` |
|
|
}) |
|
|
|
|
|
let errorMessage = `HTTP ${status}` |
|
|
if (errorData) { |
|
|
if (typeof errorData === 'string') { |
|
|
errorMessage += `: ${errorData}` |
|
|
} else if (errorData.error) { |
|
|
errorMessage += `: ${errorData.error}` |
|
|
if (errorData.error_description) { |
|
|
errorMessage += ` - ${errorData.error_description}` |
|
|
} |
|
|
} else { |
|
|
errorMessage += `: ${JSON.stringify(errorData)}` |
|
|
} |
|
|
} |
|
|
|
|
|
throw new Error(`Setup Token exchange failed: ${errorMessage}`) |
|
|
} else if (error.request) { |
|
|
logger.error('❌ Setup Token exchange failed with network error', { |
|
|
message: error.message, |
|
|
code: error.code, |
|
|
hasProxy: !!proxyConfig |
|
|
}) |
|
|
throw new Error( |
|
|
'Setup Token exchange failed: No response from server (network error or timeout)' |
|
|
) |
|
|
} else { |
|
|
logger.error('❌ Setup Token exchange failed with unknown error', { |
|
|
message: error.message, |
|
|
stack: error.stack |
|
|
}) |
|
|
throw new Error(`Setup Token exchange failed: ${error.message}`) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function formatClaudeCredentials(tokenData) { |
|
|
return { |
|
|
claudeAiOauth: { |
|
|
accessToken: tokenData.accessToken, |
|
|
refreshToken: tokenData.refreshToken, |
|
|
expiresAt: tokenData.expiresAt, |
|
|
scopes: tokenData.scopes, |
|
|
isMax: tokenData.isMax |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = { |
|
|
OAUTH_CONFIG, |
|
|
generateOAuthParams, |
|
|
generateSetupTokenParams, |
|
|
exchangeCodeForTokens, |
|
|
exchangeSetupTokenCode, |
|
|
parseCallbackUrl, |
|
|
formatClaudeCredentials, |
|
|
generateState, |
|
|
generateCodeVerifier, |
|
|
generateCodeChallenge, |
|
|
generateAuthUrl, |
|
|
generateSetupTokenAuthUrl, |
|
|
createProxyAgent |
|
|
} |
|
|
|