| |
| |
| |
| |
|
|
| 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 |
| } |
|
|