import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; import { installOAuthTokens } from '../cli/handlers/auth.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { setClipboard } from '../ink/termio/osc.js'; import { useTerminalNotification } from '../ink/useTerminalNotification.js'; import { Box, Link, Text } from '../ink.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; import { getSSLErrorHint } from '../services/api/errorUtils.js'; import { sendNotification } from '../services/notifier.js'; import { OAuthService } from '../services/oauth/index.js'; import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; import { logError } from '../utils/log.js'; import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/select.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { Spinner } from './Spinner.js'; import TextInput from './TextInput.js'; type Props = { onDone(): void; startingMessage?: string; mode?: 'login' | 'setup-token'; forceLoginMethod?: 'claudeai' | 'console'; }; type OAuthStatus = { state: 'idle'; } // Initial state, waiting to select login method | { state: 'platform_setup'; } // Show platform setup info (Bedrock/Vertex/Foundry) | { state: 'ready_to_start'; } // Flow started, waiting for browser to open | { state: 'waiting_for_login'; url: string; } // Browser opened, waiting for user to login | { state: 'creating_api_key'; } // Got access token, creating API key | { state: 'about_to_retry'; nextState: OAuthStatus; } | { state: 'success'; token?: string; } | { state: 'error'; message: string; toRetry?: OAuthStatus; }; const PASTE_HERE_MSG = 'Paste code here if prompted > '; export function ConsoleOAuthFlow({ onDone, startingMessage, mode = 'login', forceLoginMethod: forceLoginMethodProp }: Props): React.ReactNode { const settings = getSettings_DEPRECATED() || {}; const forceLoginMethod = forceLoginMethodProp ?? settings.forceLoginMethod; const orgUUID = settings.forceLoginOrgUUID; const forcedMethodMessage = forceLoginMethod === 'claudeai' ? 'Login method pre-selected: Subscription Plan (Claude Pro/Max)' : forceLoginMethod === 'console' ? 'Login method pre-selected: API Usage Billing (Anthropic Console)' : null; const terminal = useTerminalNotification(); const [oauthStatus, setOAuthStatus] = useState(() => { if (mode === 'setup-token') { return { state: 'ready_to_start' }; } if (forceLoginMethod === 'claudeai' || forceLoginMethod === 'console') { return { state: 'ready_to_start' }; } return { state: 'idle' }; }); const [pastedCode, setPastedCode] = useState(''); const [cursorOffset, setCursorOffset] = useState(0); const [oauthService] = useState(() => new OAuthService()); const [loginWithClaudeAi, setLoginWithClaudeAi] = useState(() => { // Use Claude AI auth for setup-token mode to support user:inference scope return mode === 'setup-token' || forceLoginMethod === 'claudeai'; }); // After a few seconds we suggest the user to copy/paste url if the // browser did not open automatically. In this flow we expect the user to // copy the code from the browser and paste it in the terminal const [showPastePrompt, setShowPastePrompt] = useState(false); const [urlCopied, setUrlCopied] = useState(false); const textInputColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1; // Log forced login method on mount useEffect(() => { if (forceLoginMethod === 'claudeai') { logEvent('tengu_oauth_claudeai_forced', {}); } else if (forceLoginMethod === 'console') { logEvent('tengu_oauth_console_forced', {}); } }, [forceLoginMethod]); // Retry logic useEffect(() => { if (oauthStatus.state === 'about_to_retry') { const timer = setTimeout(setOAuthStatus, 1000, oauthStatus.nextState); return () => clearTimeout(timer); } }, [oauthStatus]); // Handle Enter to continue on success state useKeybinding('confirm:yes', () => { logEvent('tengu_oauth_success', { loginWithClaudeAi }); onDone(); }, { context: 'Confirmation', isActive: oauthStatus.state === 'success' && mode !== 'setup-token' }); // Handle Enter to continue from platform setup useKeybinding('confirm:yes', () => { setOAuthStatus({ state: 'idle' }); }, { context: 'Confirmation', isActive: oauthStatus.state === 'platform_setup' }); // Handle Enter to retry on error state useKeybinding('confirm:yes', () => { if (oauthStatus.state === 'error' && oauthStatus.toRetry) { setPastedCode(''); setOAuthStatus({ state: 'about_to_retry', nextState: oauthStatus.toRetry }); } }, { context: 'Confirmation', isActive: oauthStatus.state === 'error' && !!oauthStatus.toRetry }); useEffect(() => { if (pastedCode === 'c' && oauthStatus.state === 'waiting_for_login' && showPastePrompt && !urlCopied) { void setClipboard(oauthStatus.url).then(raw => { if (raw) process.stdout.write(raw); setUrlCopied(true); setTimeout(setUrlCopied, 2000, false); }); setPastedCode(''); } }, [pastedCode, oauthStatus, showPastePrompt, urlCopied]); async function handleSubmitCode(value: string, url: string) { try { // Expecting format "authorizationCode#state" from the authorization callback URL const [authorizationCode, state] = value.split('#'); if (!authorizationCode || !state) { setOAuthStatus({ state: 'error', message: 'Invalid code. Please make sure the full code was copied', toRetry: { state: 'waiting_for_login', url } }); return; } // Track which path the user is taking (manual code entry) logEvent('tengu_oauth_manual_entry', {}); oauthService.handleManualAuthCodeInput({ authorizationCode, state }); } catch (err: unknown) { logError(err); setOAuthStatus({ state: 'error', message: (err as Error).message, toRetry: { state: 'waiting_for_login', url } }); } } const startOAuth = useCallback(async () => { try { logEvent('tengu_oauth_flow_start', { loginWithClaudeAi }); const result = await oauthService.startOAuthFlow(async url_0 => { setOAuthStatus({ state: 'waiting_for_login', url: url_0 }); setTimeout(setShowPastePrompt, 3000, true); }, { loginWithClaudeAi, inferenceOnly: mode === 'setup-token', expiresIn: mode === 'setup-token' ? 365 * 24 * 60 * 60 : undefined, // 1 year for setup-token orgUUID }).catch(err_1 => { const isTokenExchangeError = err_1.message.includes('Token exchange failed'); // Enterprise TLS proxies (Zscaler et al.) intercept the token // exchange POST and cause cryptic SSL errors. Surface an // actionable hint so the user isn't stuck in a login loop. const sslHint_0 = getSSLErrorHint(err_1); setOAuthStatus({ state: 'error', message: sslHint_0 ?? (isTokenExchangeError ? 'Failed to exchange authorization code for access token. Please try again.' : err_1.message), toRetry: mode === 'setup-token' ? { state: 'ready_to_start' } : { state: 'idle' } }); logEvent('tengu_oauth_token_exchange_error', { error: err_1.message, ssl_error: sslHint_0 !== null }); throw err_1; }); if (mode === 'setup-token') { // For setup-token mode, return the OAuth access token directly (it can be used as an API key) // Don't save to keychain - the token is displayed for manual use with CLAUDE_CODE_OAUTH_TOKEN setOAuthStatus({ state: 'success', token: result.accessToken }); } else { await installOAuthTokens(result); const orgResult = await validateForceLoginOrg(); if (!orgResult.valid) { throw new Error(orgResult.message); } setOAuthStatus({ state: 'success' }); void sendNotification({ message: 'Claude Code login successful', notificationType: 'auth_success' }, terminal); } } catch (err_0) { const errorMessage = (err_0 as Error).message; const sslHint = getSSLErrorHint(err_0); setOAuthStatus({ state: 'error', message: sslHint ?? errorMessage, toRetry: { state: mode === 'setup-token' ? 'ready_to_start' : 'idle' } }); logEvent('tengu_oauth_error', { error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, ssl_error: sslHint !== null }); } }, [oauthService, setShowPastePrompt, loginWithClaudeAi, mode, orgUUID]); const pendingOAuthStartRef = useRef(false); useEffect(() => { if (oauthStatus.state === 'ready_to_start' && !pendingOAuthStartRef.current) { pendingOAuthStartRef.current = true; process.nextTick((startOAuth_0: () => Promise, pendingOAuthStartRef_0: React.MutableRefObject) => { void startOAuth_0(); pendingOAuthStartRef_0.current = false; }, startOAuth, pendingOAuthStartRef); } }, [oauthStatus.state, startOAuth]); // Auto-exit for setup-token mode useEffect(() => { if (mode === 'setup-token' && oauthStatus.state === 'success') { // Delay to ensure static content is fully rendered before exiting const timer_0 = setTimeout((loginWithClaudeAi_0, onDone_0) => { logEvent('tengu_oauth_success', { loginWithClaudeAi: loginWithClaudeAi_0 }); // Don't clear terminal so the token remains visible onDone_0(); }, 500, loginWithClaudeAi, onDone); return () => clearTimeout(timer_0); } }, [mode, oauthStatus, loginWithClaudeAi, onDone]); // Cleanup OAuth service when component unmounts useEffect(() => { return () => { oauthService.cleanup(); }; }, [oauthService]); return {oauthStatus.state === 'waiting_for_login' && showPastePrompt && Browser didn't open? Use the url below to sign in{' '} {urlCopied ? (Copied!) : } {oauthStatus.url} } {mode === 'setup-token' && oauthStatus.state === 'success' && oauthStatus.token && ✓ Long-lived authentication token created successfully! Your OAuth token (valid for 1 year): {oauthStatus.token} Store this token securely. You won't be able to see it again. Use this token by setting: export CLAUDE_CODE_OAUTH_TOKEN=<token> } ; } type OAuthStatusMessageProps = { oauthStatus: OAuthStatus; mode: 'login' | 'setup-token'; startingMessage: string | undefined; forcedMethodMessage: string | null; showPastePrompt: boolean; pastedCode: string; setPastedCode: (value: string) => void; cursorOffset: number; setCursorOffset: (offset: number) => void; textInputColumns: number; handleSubmitCode: (value: string, url: string) => void; setOAuthStatus: (status: OAuthStatus) => void; setLoginWithClaudeAi: (value: boolean) => void; }; function OAuthStatusMessage(t0) { const $ = _c(51); const { oauthStatus, mode, startingMessage, forcedMethodMessage, showPastePrompt, pastedCode, setPastedCode, cursorOffset, setCursorOffset, textInputColumns, handleSubmitCode, setOAuthStatus, setLoginWithClaudeAi } = t0; switch (oauthStatus.state) { case "idle": { const t1 = startingMessage ? startingMessage : "Claude Code can be used with your Claude subscription or billed based on API usage through your Console account."; let t2; if ($[0] !== t1) { t2 = {t1}; $[0] = t1; $[1] = t2; } else { t2 = $[1]; } let t3; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { t3 = Select login method:; $[2] = t3; } else { t3 = $[2]; } let t4; if ($[3] === Symbol.for("react.memo_cache_sentinel")) { t4 = { label: Claude account with subscription ·{" "}Pro, Max, Team, or Enterprise{false && {"\n"}[ANT-ONLY]{" "}Please use this option unless you need to login to a special org for accessing sensitive data (e.g. customer data, HIPI data) with the Console option}{"\n"}, value: "claudeai" }; $[3] = t4; } else { t4 = $[3]; } let t5; if ($[4] === Symbol.for("react.memo_cache_sentinel")) { t5 = { label: Anthropic Console account ·{" "}API usage billing{"\n"}, value: "console" }; $[4] = t5; } else { t5 = $[4]; } let t6; if ($[5] === Symbol.for("react.memo_cache_sentinel")) { t6 = [t4, t5, { label: 3rd-party platform ·{" "}Amazon Bedrock, Microsoft Foundry, or Vertex AI{"\n"}, value: "platform" }]; $[5] = t6; } else { t6 = $[5]; } let t7; if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) { t7 =