File size: 3,318 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
import { OAuthService } from "./oauth.js";
import { OPENAI_CONFIG } from "../constants/oauth.js";
import { getServerCredentials } from "../config/index.js";
import { spinner as createSpinner } from "../utils/ui.js";

/**
 * OpenAI OAuth Service (Native)
 * Uses Authorization Code Flow with PKCE (similar to Codex)
 */
export class OpenAIService extends OAuthService {
  constructor() {
    super(OPENAI_CONFIG);
  }

  /**
   * Build OpenAI authorization URL
   */
  buildOpenAIAuthUrl(redirectUri, state, codeChallenge) {
    const params = new URLSearchParams({
      client_id: OPENAI_CONFIG.clientId,
      response_type: "code",
      redirect_uri: redirectUri,
      scope: OPENAI_CONFIG.scope,
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: OPENAI_CONFIG.codeChallengeMethod,
      ...OPENAI_CONFIG.extraParams,
    });

    return `${OPENAI_CONFIG.authorizeUrl}?${params.toString()}`;
  }

  /**
   * Exchange OpenAI authorization code for tokens
   */
  async exchangeOpenAICode(code, redirectUri, codeVerifier) {
    const response = await fetch(OPENAI_CONFIG.tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: OPENAI_CONFIG.clientId,
        code: code,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    return await response.json();
  }

  /**
   * Save OpenAI tokens to server
   */
  async saveTokens(tokens) {
    const { server, token, userId } = getServerCredentials();

    const response = await fetch(`${server}/api/cli/providers/openai`, {
      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,
        idToken: tokens.id_token,
        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 OpenAI OAuth flow
   */
  async connect() {
    const spinner = createSpinner("Starting OpenAI OAuth...").start();

    try {
      spinner.text = "Starting local server...";

      // Authenticate and get authorization code
      const { code, codeVerifier, redirectUri } = await this.authenticate(
        "OpenAI",
        this.buildOpenAIAuthUrl.bind(this)
      );

      spinner.start("Exchanging code for tokens...");

      // Exchange code for tokens
      const tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier);

      spinner.text = "Saving tokens to server...";

      // Save tokens to server
      await this.saveTokens(tokens);

      spinner.succeed("OpenAI connected successfully!");
      return true;
    } catch (error) {
      spinner.fail(`Failed: ${error.message}`);
      throw error;
    }
  }
}