import { OAuthService } from "./oauth.js"; import { CLAUDE_CONFIG } from "../constants/oauth.js"; import { getServerCredentials } from "../config/index.js"; import { spinner as createSpinner } from "../utils/ui.js"; /** * Claude OAuth Service */ export class ClaudeService extends OAuthService { constructor() { super(CLAUDE_CONFIG); } /** * Build Claude authorization URL */ buildClaudeAuthUrl(redirectUri, state, codeChallenge) { const scopeStr = CLAUDE_CONFIG.scopes.join(" "); const params = new URLSearchParams({ code: "true", client_id: CLAUDE_CONFIG.clientId, response_type: "code", redirect_uri: redirectUri, scope: scopeStr, code_challenge: codeChallenge, code_challenge_method: CLAUDE_CONFIG.codeChallengeMethod, state: state, }); return `${CLAUDE_CONFIG.authorizeUrl}?${params.toString()}`; } /** * Exchange Claude authorization code (with special handling) */ async exchangeClaudeCode(code, redirectUri, codeVerifier, state) { // Parse code - may contain state after # let authCode = code; let codeState = ""; if (authCode.includes("#")) { const parts = authCode.split("#"); authCode = parts[0]; codeState = parts[1] || ""; } // Claude uses JSON format (not form-urlencoded) const tokenPayload = { code: authCode, state: codeState || state, grant_type: "authorization_code", client_id: CLAUDE_CONFIG.clientId, redirect_uri: redirectUri, code_verifier: codeVerifier, }; const response = await fetch(CLAUDE_CONFIG.tokenUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(tokenPayload), }); if (!response.ok) { const error = await response.text(); throw new Error(`Token exchange failed: ${error}`); } return await response.json(); } /** * Save Claude tokens to server */ async saveTokens(tokens) { const { server, token, userId } = getServerCredentials(); // Server will auto-generate displayName based on existing account count const response = await fetch(`${server}/api/cli/providers/claude`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "X-User-Id": userId, }, body: JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresIn: tokens.expires_in, scope: tokens.scope, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to save tokens"); } return await response.json(); } /** * Complete Claude OAuth flow */ async connect() { const spinner = createSpinner("Starting Claude OAuth...").start(); try { spinner.text = "Starting local server..."; // Authenticate and get authorization code const { code, state, codeVerifier, redirectUri } = await this.authenticate( "Claude", this.buildClaudeAuthUrl.bind(this) ); spinner.start("Exchanging code for tokens..."); // Exchange code for tokens const tokens = await this.exchangeClaudeCode(code, redirectUri, codeVerifier, state); spinner.text = "Saving tokens to server..."; // Save tokens to server await this.saveTokens(tokens); spinner.succeed("Claude connected successfully!"); return true; } catch (error) { spinner.fail(`Failed: ${error.message}`); throw error; } } }