|
// /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; |
Well, there are two ways. You can try to add corresponding headers to the request (ones, specified in the openai api spec) or can just try copilot cli, newly made by GitHub, I think they have such preference configurable