Initial commit
This commit is contained in:
225
src/lib/oauth/services/github.js
Normal file
225
src/lib/oauth/services/github.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user