|
// /home/user/.claude-code-router/auth/github-copilot.js |
|
const fs = require("fs"); |
|
const path = require("path"); |
|
|
|
class GitHubCopilotAuth { |
|
constructor() { |
|
this.CLIENT_ID = "01ab8ac9400c4e429b23"; |
|
this.DEVICE_CODE_URL = "https://github.com/login/device/code"; |
|
this.ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; |
|
this.COPILOT_API_KEY_URL = |
|
"https://api.github.com/copilot_internal/v2/token"; |
|
this.TOKEN_FILE_PATH = process.env.COPILOT_TOKEN_FILE || path.join( |
|
process.env.HOME || process.env.USERPROFILE, |
|
".copilot-tokens.json" |
|
); |
|
} |
|
|
|
async startDeviceFlow() { |
|
const response = await fetch(this.DEVICE_CODE_URL, { |
|
method: "POST", |
|
headers: { |
|
Accept: "application/json", |
|
"Content-Type": "application/json", |
|
"User-Agent": "GitHubCopilotChat/0.26.7", |
|
}, |
|
body: JSON.stringify({ |
|
client_id: this.CLIENT_ID, |
|
scope: "read:user", |
|
}), |
|
}); |
|
|
|
const data = await response.json(); |
|
return { |
|
deviceCode: data.device_code, |
|
userCode: data.user_code, |
|
verificationUri: data.verification_uri, |
|
interval: data.interval || 5, |
|
expiresIn: data.expires_in, |
|
}; |
|
} |
|
|
|
async pollForToken(deviceCode) { |
|
const response = await fetch(this.ACCESS_TOKEN_URL, { |
|
method: "POST", |
|
headers: { |
|
Accept: "application/json", |
|
"Content-Type": "application/json", |
|
"User-Agent": "GitHubCopilotChat/0.26.7", |
|
}, |
|
body: JSON.stringify({ |
|
client_id: this.CLIENT_ID, |
|
device_code: deviceCode, |
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code", |
|
}), |
|
}); |
|
|
|
const data = await response.json(); |
|
|
|
if (data.access_token) { |
|
return { success: true, accessToken: data.access_token }; |
|
} |
|
|
|
if (data.error === "authorization_pending") { |
|
return { pending: true }; |
|
} |
|
|
|
return { error: data.error }; |
|
} |
|
|
|
async getCopilotToken(githubAccessToken) { |
|
const response = await fetch(this.COPILOT_API_KEY_URL, { |
|
headers: { |
|
Accept: "application/json", |
|
Authorization: `Bearer ${githubAccessToken}`, |
|
"User-Agent": "GitHubCopilotChat/0.26.7", |
|
"Editor-Version": "vscode/1.99.3", |
|
"Editor-Plugin-Version": "copilot-chat/0.26.7", |
|
}, |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`Failed to get Copilot token: ${response.statusText}`); |
|
} |
|
|
|
const tokenData = await response.json(); |
|
return { |
|
token: tokenData.token, |
|
expiresAt: tokenData.expires_at, |
|
endpoint: |
|
tokenData.endpoints?.api || |
|
"https://copilot-proxy.githubusercontent.com/v1/engines/copilot-codex/completions", |
|
}; |
|
} |
|
|
|
isTokenExpired(bufferMinutes = 5) { |
|
try { |
|
const tokenFile = this.TOKEN_FILE_PATH; |
|
if (!fs.existsSync(tokenFile)) { |
|
return true; |
|
} |
|
|
|
const data = JSON.parse(fs.readFileSync(tokenFile, "utf8")); |
|
if (!data.expiresAt) { |
|
return true; |
|
} |
|
|
|
const now = Math.floor(Date.now() / 1000); |
|
const bufferTime = bufferMinutes * 60; |
|
|
|
return now >= data.expiresAt - bufferTime; |
|
} catch (error) { |
|
console.error("Error checking token expiration:", error); |
|
return true; |
|
} |
|
} |
|
|
|
getTokenFromFile() { |
|
const tokenFile = this.TOKEN_FILE_PATH; |
|
if (!fs.existsSync(tokenFile)) { |
|
return; |
|
} |
|
|
|
const data = JSON.parse(fs.readFileSync(tokenFile, "utf8")); |
|
return data; |
|
} |
|
|
|
updateTokenFile(tokenData) { |
|
try { |
|
const tokenFile = this.TOKEN_FILE_PATH; |
|
fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2)); |
|
} catch (error) { |
|
console.error("Error updating token files:", error); |
|
} |
|
} |
|
|
|
async refreshCopilotToken() { |
|
try { |
|
const existingData = this.getTokenFromFile(); |
|
if (!existingData.githubToken) { |
|
throw new Error("No GitHub token found. Please re-authenticate."); |
|
} |
|
|
|
console.log("Refreshing Copilot token..."); |
|
const copilotToken = await this.getCopilotToken(existingData.githubToken); |
|
|
|
const tokenData = { |
|
githubToken: existingData.githubToken, |
|
copilotToken: copilotToken.token, |
|
endpoint: `${copilotToken.endpoint}/chat/completions`, |
|
expiresAt: copilotToken.expiresAt, |
|
lastUpdated: new Date().toISOString(), |
|
}; |
|
|
|
this.updateTokenFile(tokenData); |
|
console.log("Copilot token refreshed successfully!"); |
|
|
|
return tokenData; |
|
} catch (error) { |
|
throw new Error(`Failed to refresh Copilot token: ${error.message}`); |
|
} |
|
} |
|
} |
|
|
|
module.exports = GitHubCopilotAuth; |