| import { OAuthService } from "./oauth.js"; |
| import { GITHUB_CONFIG } from "../constants/oauth.js"; |
| import { spinner as createSpinner } from "../utils/ui.js"; |
|
|
| |
| |
| |
| |
| export class GitHubService extends OAuthService { |
| constructor() { |
| super(GITHUB_CONFIG); |
| } |
|
|
| |
| |
| |
| async getDeviceCode() { |
| const response = await fetch(`${GITHUB_CONFIG.deviceCodeUrl}`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/x-www-form-urlencoded", |
| Accept: "application/json", |
| }, |
| body: new URLSearchParams({ |
| client_id: GITHUB_CONFIG.clientId, |
| scope: GITHUB_CONFIG.scopes, |
| }), |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(`Failed to get device code: ${error}`); |
| } |
|
|
| return await response.json(); |
| } |
|
|
| |
| |
| |
| async pollAccessToken(deviceCode, verificationUri, userCode, interval = 5000) { |
| const spinner = createSpinner("Waiting for GitHub authentication...").start(); |
| |
| |
| console.log(`\nPlease visit: ${verificationUri}`); |
| console.log(`Enter code: ${userCode}\n`); |
| |
| |
| try { |
| const open = (await import("open")).default; |
| await open(verificationUri); |
| } catch (error) { |
| console.log("Could not open browser automatically. Please visit the URL above manually."); |
| } |
|
|
| |
| while (true) { |
| await new Promise(resolve => setTimeout(resolve, interval)); |
|
|
| const response = await fetch(`${GITHUB_CONFIG.tokenUrl}`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/x-www-form-urlencoded", |
| Accept: "application/json", |
| }, |
| body: new URLSearchParams({ |
| client_id: GITHUB_CONFIG.clientId, |
| device_code: deviceCode, |
| grant_type: "urn:ietf:params:oauth:grant-type:device_code", |
| }), |
| }); |
|
|
| const data = await response.json(); |
|
|
| if (data.access_token) { |
| spinner.succeed("GitHub authentication successful!"); |
| return { |
| access_token: data.access_token, |
| token_type: data.token_type, |
| scope: data.scope, |
| }; |
| } else if (data.error === "authorization_pending") { |
| |
| continue; |
| } else if (data.error === "slow_down") { |
| |
| interval += 5000; |
| continue; |
| } else if (data.error === "expired_token") { |
| spinner.fail("Device code expired. Please try again."); |
| throw new Error("Device code expired"); |
| } else if (data.error === "access_denied") { |
| spinner.fail("Access denied by user."); |
| throw new Error("Access denied"); |
| } else { |
| spinner.fail("Failed to get access token."); |
| throw new Error(data.error_description || data.error); |
| } |
| } |
| } |
|
|
| |
| |
| |
| async getCopilotToken(accessToken) { |
| const response = await fetch(`${GITHUB_CONFIG.copilotTokenUrl}`, { |
| headers: { |
| Authorization: `Bearer ${accessToken}`, |
| Accept: "application/json", |
| "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, |
| "User-Agent": GITHUB_CONFIG.userAgent, |
| }, |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(`Failed to get Copilot token: ${error}`); |
| } |
|
|
| return await response.json(); |
| } |
|
|
| |
| |
| |
| async getUserInfo(accessToken) { |
| const response = await fetch(`${GITHUB_CONFIG.userInfoUrl}`, { |
| headers: { |
| Authorization: `Bearer ${accessToken}`, |
| Accept: "application/json", |
| "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, |
| "User-Agent": GITHUB_CONFIG.userAgent, |
| }, |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(`Failed to get user info: ${error}`); |
| } |
|
|
| return await response.json(); |
| } |
|
|
| |
| |
| |
| async authenticate() { |
| try { |
| |
| const deviceResponse = await this.getDeviceCode(); |
| |
| |
| const tokenResponse = await this.pollAccessToken( |
| deviceResponse.device_code, |
| deviceResponse.verification_uri, |
| deviceResponse.user_code |
| ); |
| |
| |
| const copilotToken = await this.getCopilotToken(tokenResponse.access_token); |
| |
| |
| const userInfo = await this.getUserInfo(tokenResponse.access_token); |
| |
| console.log(`\n✅ Successfully authenticated as ${userInfo.login}`); |
| |
| return { |
| accessToken: tokenResponse.access_token, |
| copilotToken: copilotToken.token, |
| refreshToken: null, |
| expiresIn: copilotToken.expires_at, |
| userInfo: { |
| id: userInfo.id, |
| login: userInfo.login, |
| name: userInfo.name, |
| email: userInfo.email, |
| }, |
| copilotTokenInfo: copilotToken, |
| }; |
| } catch (error) { |
| throw new Error(`GitHub authentication failed: ${error.message}`); |
| } |
| } |
|
|
| |
| |
| |
| async connect() { |
| try { |
| |
| const authResult = await this.authenticate(); |
| |
| |
| const { server, token, userId } = await import("../config/index.js").then(m => m.getServerCredentials()); |
| const spinner = (await import("../utils/ui.js")).spinner("Connecting to server...").start(); |
| |
| const response = await fetch(`${server}/api/cli/providers/github`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Authorization: `Bearer ${token}`, |
| "X-User-Id": userId, |
| }, |
| body: JSON.stringify({ |
| accessToken: authResult.accessToken, |
| copilotToken: authResult.copilotToken, |
| userInfo: authResult.userInfo, |
| copilotTokenInfo: authResult.copilotTokenInfo, |
| }), |
| }); |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || "Failed to connect to server"); |
| } |
| |
| spinner.succeed("GitHub Copilot connected successfully!"); |
| console.log(`\nConnected as: ${authResult.userInfo.login}`); |
| } catch (error) { |
| const { error: showError } = await import("../utils/ui.js"); |
| showError(`GitHub connection failed: ${error.message}`); |
| throw error; |
| } |
| } |
| } |
|
|