| |
|
|
| import { |
| clearAuthRelatedCaches, |
| performLogout, |
| } from '../../commands/logout/logout.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from '../../services/analytics/index.js' |
| import { getSSLErrorHint } from '../../services/api/errorUtils.js' |
| import { fetchAndStoreClaudeCodeFirstTokenDate } from '../../services/api/firstTokenDate.js' |
| import { |
| createAndStoreApiKey, |
| fetchAndStoreUserRoles, |
| refreshOAuthToken, |
| shouldUseClaudeAIAuth, |
| storeOAuthAccountInfo, |
| } from '../../services/oauth/client.js' |
| import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js' |
| import { OAuthService } from '../../services/oauth/index.js' |
| import type { OAuthTokens } from '../../services/oauth/types.js' |
| import { |
| clearOAuthTokenCache, |
| getAnthropicApiKeyWithSource, |
| getAuthTokenSource, |
| getOauthAccountInfo, |
| getSubscriptionType, |
| isUsing3PServices, |
| saveOAuthTokensIfNeeded, |
| validateForceLoginOrg, |
| } from '../../utils/auth.js' |
| import { saveGlobalConfig } from '../../utils/config.js' |
| import { logForDebugging } from '../../utils/debug.js' |
| import { isRunningOnHomespace } from '../../utils/envUtils.js' |
| import { errorMessage } from '../../utils/errors.js' |
| import { logError } from '../../utils/log.js' |
| import { getAPIProvider } from '../../utils/model/providers.js' |
| import { getInitialSettings } from '../../utils/settings/settings.js' |
| import { jsonStringify } from '../../utils/slowOperations.js' |
| import { |
| buildAccountProperties, |
| buildAPIProviderProperties, |
| } from '../../utils/status.js' |
|
|
| |
| |
| |
| |
| export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> { |
| |
| await performLogout({ clearOnboarding: false }) |
|
|
| |
| const profile = |
| tokens.profile ?? (await getOauthProfileFromOauthToken(tokens.accessToken)) |
| if (profile) { |
| storeOAuthAccountInfo({ |
| accountUuid: profile.account.uuid, |
| emailAddress: profile.account.email, |
| organizationUuid: profile.organization.uuid, |
| displayName: profile.account.display_name || undefined, |
| hasExtraUsageEnabled: |
| profile.organization.has_extra_usage_enabled ?? undefined, |
| billingType: profile.organization.billing_type ?? undefined, |
| subscriptionCreatedAt: |
| profile.organization.subscription_created_at ?? undefined, |
| accountCreatedAt: profile.account.created_at, |
| }) |
| } else if (tokens.tokenAccount) { |
| |
| storeOAuthAccountInfo({ |
| accountUuid: tokens.tokenAccount.uuid, |
| emailAddress: tokens.tokenAccount.emailAddress, |
| organizationUuid: tokens.tokenAccount.organizationUuid, |
| }) |
| } |
|
|
| const storageResult = saveOAuthTokensIfNeeded(tokens) |
| clearOAuthTokenCache() |
|
|
| if (storageResult.warning) { |
| logEvent('tengu_oauth_storage_warning', { |
| warning: |
| storageResult.warning as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
|
|
| |
| |
| await fetchAndStoreUserRoles(tokens.accessToken).catch(err => |
| logForDebugging(String(err), { level: 'error' }), |
| ) |
|
|
| if (shouldUseClaudeAIAuth(tokens.scopes)) { |
| await fetchAndStoreClaudeCodeFirstTokenDate().catch(err => |
| logForDebugging(String(err), { level: 'error' }), |
| ) |
| } else { |
| |
| const apiKey = await createAndStoreApiKey(tokens.accessToken) |
| if (!apiKey) { |
| throw new Error( |
| 'Unable to create API key. The server accepted the request but did not return a key.', |
| ) |
| } |
| } |
|
|
| await clearAuthRelatedCaches() |
| } |
|
|
| export async function authLogin({ |
| email, |
| sso, |
| console: useConsole, |
| claudeai, |
| }: { |
| email?: string |
| sso?: boolean |
| console?: boolean |
| claudeai?: boolean |
| }): Promise<void> { |
| if (useConsole && claudeai) { |
| process.stderr.write( |
| 'Error: --console and --claudeai cannot be used together.\n', |
| ) |
| process.exit(1) |
| } |
|
|
| const settings = getInitialSettings() |
| |
| |
| const loginWithClaudeAi = settings.forceLoginMethod |
| ? settings.forceLoginMethod === 'claudeai' |
| : !useConsole |
| const orgUUID = settings.forceLoginOrgUUID |
|
|
| |
| |
| const envRefreshToken = process.env.CLAUDE_CODE_OAUTH_REFRESH_TOKEN |
| if (envRefreshToken) { |
| const envScopes = process.env.CLAUDE_CODE_OAUTH_SCOPES |
| if (!envScopes) { |
| process.stderr.write( |
| 'CLAUDE_CODE_OAUTH_SCOPES is required when using CLAUDE_CODE_OAUTH_REFRESH_TOKEN.\n' + |
| 'Set it to the space-separated scopes the refresh token was issued with\n' + |
| '(e.g. "user:inference" or "user:profile user:inference user:sessions:claude_code user:mcp_servers").\n', |
| ) |
| process.exit(1) |
| } |
|
|
| const scopes = envScopes.split(/\s+/).filter(Boolean) |
|
|
| try { |
| logEvent('tengu_login_from_refresh_token', {}) |
|
|
| const tokens = await refreshOAuthToken(envRefreshToken, { scopes }) |
| await installOAuthTokens(tokens) |
|
|
| const orgResult = await validateForceLoginOrg() |
| if (!orgResult.valid) { |
| process.stderr.write(orgResult.message + '\n') |
| process.exit(1) |
| } |
|
|
| |
| |
| saveGlobalConfig(current => { |
| if (current.hasCompletedOnboarding) return current |
| return { ...current, hasCompletedOnboarding: true } |
| }) |
|
|
| logEvent('tengu_oauth_success', { |
| loginWithClaudeAi: shouldUseClaudeAIAuth(tokens.scopes), |
| }) |
| process.stdout.write('Login successful.\n') |
| process.exit(0) |
| } catch (err) { |
| logError(err) |
| const sslHint = getSSLErrorHint(err) |
| process.stderr.write( |
| `Login failed: ${errorMessage(err)}\n${sslHint ? sslHint + '\n' : ''}`, |
| ) |
| process.exit(1) |
| } |
| } |
|
|
| const resolvedLoginMethod = sso ? 'sso' : undefined |
|
|
| const oauthService = new OAuthService() |
|
|
| try { |
| logEvent('tengu_oauth_flow_start', { loginWithClaudeAi }) |
|
|
| const result = await oauthService.startOAuthFlow( |
| async url => { |
| process.stdout.write('Opening browser to sign in…\n') |
| process.stdout.write(`If the browser didn't open, visit: ${url}\n`) |
| }, |
| { |
| loginWithClaudeAi, |
| loginHint: email, |
| loginMethod: resolvedLoginMethod, |
| orgUUID, |
| }, |
| ) |
|
|
| await installOAuthTokens(result) |
|
|
| const orgResult = await validateForceLoginOrg() |
| if (!orgResult.valid) { |
| process.stderr.write(orgResult.message + '\n') |
| process.exit(1) |
| } |
|
|
| logEvent('tengu_oauth_success', { loginWithClaudeAi }) |
|
|
| process.stdout.write('Login successful.\n') |
| process.exit(0) |
| } catch (err) { |
| logError(err) |
| const sslHint = getSSLErrorHint(err) |
| process.stderr.write( |
| `Login failed: ${errorMessage(err)}\n${sslHint ? sslHint + '\n' : ''}`, |
| ) |
| process.exit(1) |
| } finally { |
| oauthService.cleanup() |
| } |
| } |
|
|
| export async function authStatus(opts: { |
| json?: boolean |
| text?: boolean |
| }): Promise<void> { |
| const { source: authTokenSource, hasToken } = getAuthTokenSource() |
| const { source: apiKeySource } = getAnthropicApiKeyWithSource() |
| const hasApiKeyEnvVar = |
| !!process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace() |
| const oauthAccount = getOauthAccountInfo() |
| const subscriptionType = getSubscriptionType() |
| const using3P = isUsing3PServices() |
| const loggedIn = |
| hasToken || apiKeySource !== 'none' || hasApiKeyEnvVar || using3P |
|
|
| |
| let authMethod: string = 'none' |
| if (using3P) { |
| authMethod = 'third_party' |
| } else if (authTokenSource === 'claude.ai') { |
| authMethod = 'claude.ai' |
| } else if (authTokenSource === 'apiKeyHelper') { |
| authMethod = 'api_key_helper' |
| } else if (authTokenSource !== 'none') { |
| authMethod = 'oauth_token' |
| } else if (apiKeySource === 'ANTHROPIC_API_KEY' || hasApiKeyEnvVar) { |
| authMethod = 'api_key' |
| } else if (apiKeySource === '/login managed key') { |
| authMethod = 'claude.ai' |
| } |
|
|
| if (opts.text) { |
| const properties = [ |
| ...buildAccountProperties(), |
| ...buildAPIProviderProperties(), |
| ] |
| let hasAuthProperty = false |
| for (const prop of properties) { |
| const value = |
| typeof prop.value === 'string' |
| ? prop.value |
| : Array.isArray(prop.value) |
| ? prop.value.join(', ') |
| : null |
| if (value === null || value === 'none') { |
| continue |
| } |
| hasAuthProperty = true |
| if (prop.label) { |
| process.stdout.write(`${prop.label}: ${value}\n`) |
| } else { |
| process.stdout.write(`${value}\n`) |
| } |
| } |
| if (!hasAuthProperty && hasApiKeyEnvVar) { |
| process.stdout.write('API key: ANTHROPIC_API_KEY\n') |
| } |
| if (!loggedIn) { |
| process.stdout.write( |
| 'Not logged in. Run claude auth login to authenticate.\n', |
| ) |
| } |
| } else { |
| const apiProvider = getAPIProvider() |
| const resolvedApiKeySource = |
| apiKeySource !== 'none' |
| ? apiKeySource |
| : hasApiKeyEnvVar |
| ? 'ANTHROPIC_API_KEY' |
| : null |
| const output: Record<string, string | boolean | null> = { |
| loggedIn, |
| authMethod, |
| apiProvider, |
| } |
| if (resolvedApiKeySource) { |
| output.apiKeySource = resolvedApiKeySource |
| } |
| if (authMethod === 'claude.ai') { |
| output.email = oauthAccount?.emailAddress ?? null |
| output.orgId = oauthAccount?.organizationUuid ?? null |
| output.orgName = oauthAccount?.organizationName ?? null |
| output.subscriptionType = subscriptionType ?? null |
| } |
|
|
| process.stdout.write(jsonStringify(output, null, 2) + '\n') |
| } |
| process.exit(loggedIn ? 0 : 1) |
| } |
|
|
| export async function authLogout(): Promise<void> { |
| try { |
| await performLogout({ clearOnboarding: false }) |
| } catch { |
| process.stderr.write('Failed to log out.\n') |
| process.exit(1) |
| } |
| process.stdout.write('Successfully logged out from your Anthropic account.\n') |
| process.exit(0) |
| } |
|
|