File size: 3,685 Bytes
88c4c60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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;
    }
  }
}