Spaces:
Sleeping
Sleeping
| import { OAuthService } from "./oauth.js"; | |
| import { GITHUB_CONFIG } from "../constants/oauth.js"; | |
| import { spinner as createSpinner } from "../utils/ui.js"; | |
| /** | |
| * GitHub Copilot OAuth Service | |
| * Uses Device Code Flow for authentication | |
| */ | |
| export class GitHubService extends OAuthService { | |
| constructor() { | |
| super(GITHUB_CONFIG); | |
| } | |
| /** | |
| * Get device code for GitHub authentication | |
| */ | |
| 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(); | |
| } | |
| /** | |
| * Poll for access token using device code | |
| */ | |
| async pollAccessToken(deviceCode, verificationUri, userCode, interval = 5000) { | |
| const spinner = createSpinner("Waiting for GitHub authentication...").start(); | |
| // Show user code and verification URL | |
| console.log(`\nPlease visit: ${verificationUri}`); | |
| console.log(`Enter code: ${userCode}\n`); | |
| // Open browser automatically | |
| 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."); | |
| } | |
| // Poll for access token | |
| 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 polling | |
| continue; | |
| } else if (data.error === "slow_down") { | |
| // Increase polling interval | |
| 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); | |
| } | |
| } | |
| } | |
| /** | |
| * Get Copilot token using GitHub access token | |
| */ | |
| async getCopilotToken(accessToken) { | |
| const response = await fetch(`${GITHUB_CONFIG.copilotTokenUrl}`, { | |
| headers: { | |
| Authorization: `Bearer ${accessToken}`, // GitHub API typically uses Bearer | |
| 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(); | |
| } | |
| /** | |
| * Get user info using GitHub access token | |
| */ | |
| async getUserInfo(accessToken) { | |
| const response = await fetch(`${GITHUB_CONFIG.userInfoUrl}`, { | |
| headers: { | |
| Authorization: `Bearer ${accessToken}`, // GitHub API typically uses Bearer | |
| 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(); | |
| } | |
| /** | |
| * Complete GitHub Copilot authentication flow | |
| */ | |
| async authenticate() { | |
| try { | |
| // Get device code | |
| const deviceResponse = await this.getDeviceCode(); | |
| // Poll for access token | |
| const tokenResponse = await this.pollAccessToken( | |
| deviceResponse.device_code, | |
| deviceResponse.verification_uri, | |
| deviceResponse.user_code | |
| ); | |
| // Get Copilot token | |
| const copilotToken = await this.getCopilotToken(tokenResponse.access_token); | |
| // Get user info | |
| 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, // GitHub device flow doesn't return refresh token | |
| 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}`); | |
| } | |
| } | |
| /** | |
| * Connect to server with GitHub credentials | |
| */ | |
| async connect() { | |
| try { | |
| // Authenticate with GitHub | |
| const authResult = await this.authenticate(); | |
| // Send credentials to server | |
| 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; | |
| } | |
| } | |
| } | |