| 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"; |
|
|
| |
| |
| |
| export class ClaudeService extends OAuthService { |
| constructor() { |
| super(CLAUDE_CONFIG); |
| } |
|
|
| |
| |
| |
| 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()}`; |
| } |
|
|
| |
| |
| |
| async exchangeClaudeCode(code, redirectUri, codeVerifier, state) { |
| |
| let authCode = code; |
| let codeState = ""; |
| if (authCode.includes("#")) { |
| const parts = authCode.split("#"); |
| authCode = parts[0]; |
| codeState = parts[1] || ""; |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| |
| |
| async saveTokens(tokens) { |
| const { server, token, userId } = getServerCredentials(); |
|
|
| |
| 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(); |
| } |
|
|
| |
| |
| |
| async connect() { |
| const spinner = createSpinner("Starting Claude OAuth...").start(); |
|
|
| try { |
| spinner.text = "Starting local server..."; |
|
|
| |
| const { code, state, codeVerifier, redirectUri } = await this.authenticate( |
| "Claude", |
| this.buildClaudeAuthUrl.bind(this) |
| ); |
|
|
| spinner.start("Exchanging code for tokens..."); |
|
|
| |
| const tokens = await this.exchangeClaudeCode(code, redirectUri, codeVerifier, state); |
|
|
| spinner.text = "Saving tokens to server..."; |
|
|
| |
| await this.saveTokens(tokens); |
|
|
| spinner.succeed("Claude connected successfully!"); |
| return true; |
| } catch (error) { |
| spinner.fail(`Failed: ${error.message}`); |
| throw error; |
| } |
| } |
| } |
|
|
|
|