Spaces:
Configuration error
Configuration error
| import path from "node:path"; | |
| import { resolveStateDir } from "../config/paths.js"; | |
| import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; | |
| const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token"; | |
| export type CachedCopilotToken = { | |
| token: string; | |
| /** milliseconds since epoch */ | |
| expiresAt: number; | |
| /** milliseconds since epoch */ | |
| updatedAt: number; | |
| }; | |
| function resolveCopilotTokenCachePath(env: NodeJS.ProcessEnv = process.env) { | |
| return path.join(resolveStateDir(env), "credentials", "github-copilot.token.json"); | |
| } | |
| function isTokenUsable(cache: CachedCopilotToken, now = Date.now()): boolean { | |
| // Keep a small safety margin when checking expiry. | |
| return cache.expiresAt - now > 5 * 60 * 1000; | |
| } | |
| function parseCopilotTokenResponse(value: unknown): { | |
| token: string; | |
| expiresAt: number; | |
| } { | |
| if (!value || typeof value !== "object") { | |
| throw new Error("Unexpected response from GitHub Copilot token endpoint"); | |
| } | |
| const asRecord = value as Record<string, unknown>; | |
| const token = asRecord.token; | |
| const expiresAt = asRecord.expires_at; | |
| if (typeof token !== "string" || token.trim().length === 0) { | |
| throw new Error("Copilot token response missing token"); | |
| } | |
| // GitHub returns a unix timestamp (seconds), but we defensively accept ms too. | |
| let expiresAtMs: number; | |
| if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) { | |
| expiresAtMs = expiresAt > 10_000_000_000 ? expiresAt : expiresAt * 1000; | |
| } else if (typeof expiresAt === "string" && expiresAt.trim().length > 0) { | |
| const parsed = Number.parseInt(expiresAt, 10); | |
| if (!Number.isFinite(parsed)) { | |
| throw new Error("Copilot token response has invalid expires_at"); | |
| } | |
| expiresAtMs = parsed > 10_000_000_000 ? parsed : parsed * 1000; | |
| } else { | |
| throw new Error("Copilot token response missing expires_at"); | |
| } | |
| return { token, expiresAt: expiresAtMs }; | |
| } | |
| export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com"; | |
| export function deriveCopilotApiBaseUrlFromToken(token: string): string | null { | |
| const trimmed = token.trim(); | |
| if (!trimmed) return null; | |
| // The token returned from the Copilot token endpoint is a semicolon-delimited | |
| // set of key/value pairs. One of them is `proxy-ep=...`. | |
| const match = trimmed.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i); | |
| const proxyEp = match?.[1]?.trim(); | |
| if (!proxyEp) return null; | |
| // pi-ai expects converting proxy.* -> api.* | |
| // (see upstream getGitHubCopilotBaseUrl). | |
| const host = proxyEp.replace(/^https?:\/\//, "").replace(/^proxy\./i, "api."); | |
| if (!host) return null; | |
| return `https://${host}`; | |
| } | |
| export async function resolveCopilotApiToken(params: { | |
| githubToken: string; | |
| env?: NodeJS.ProcessEnv; | |
| fetchImpl?: typeof fetch; | |
| }): Promise<{ | |
| token: string; | |
| expiresAt: number; | |
| source: string; | |
| baseUrl: string; | |
| }> { | |
| const env = params.env ?? process.env; | |
| const cachePath = resolveCopilotTokenCachePath(env); | |
| const cached = loadJsonFile(cachePath) as CachedCopilotToken | undefined; | |
| if (cached && typeof cached.token === "string" && typeof cached.expiresAt === "number") { | |
| if (isTokenUsable(cached)) { | |
| return { | |
| token: cached.token, | |
| expiresAt: cached.expiresAt, | |
| source: `cache:${cachePath}`, | |
| baseUrl: deriveCopilotApiBaseUrlFromToken(cached.token) ?? DEFAULT_COPILOT_API_BASE_URL, | |
| }; | |
| } | |
| } | |
| const fetchImpl = params.fetchImpl ?? fetch; | |
| const res = await fetchImpl(COPILOT_TOKEN_URL, { | |
| method: "GET", | |
| headers: { | |
| Accept: "application/json", | |
| Authorization: `Bearer ${params.githubToken}`, | |
| }, | |
| }); | |
| if (!res.ok) { | |
| throw new Error(`Copilot token exchange failed: HTTP ${res.status}`); | |
| } | |
| const json = parseCopilotTokenResponse(await res.json()); | |
| const payload: CachedCopilotToken = { | |
| token: json.token, | |
| expiresAt: json.expiresAt, | |
| updatedAt: Date.now(), | |
| }; | |
| saveJsonFile(cachePath, payload); | |
| return { | |
| token: payload.token, | |
| expiresAt: payload.expiresAt, | |
| source: `fetched:${COPILOT_TOKEN_URL}`, | |
| baseUrl: deriveCopilotApiBaseUrlFromToken(payload.token) ?? DEFAULT_COPILOT_API_BASE_URL, | |
| }; | |
| } | |