93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
import path from "node:path";
|
|
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
|
|
import { resolveStateDir } from "../config/paths.js";
|
|
|
|
export function deriveCopilotApiBaseUrlFromToken(token: string): string {
|
|
const m = /proxy-ep=([^;]+)/.exec(token || "");
|
|
if (!m) return "https://api.github.com";
|
|
let ep = m[1];
|
|
// ensure we have a URL-like string
|
|
let proto = "https:";
|
|
let host = ep;
|
|
try {
|
|
if (/^https?:\/\//i.test(ep)) {
|
|
const u = new URL(ep);
|
|
proto = u.protocol;
|
|
host = u.hostname;
|
|
}
|
|
} catch {
|
|
// leave as-is
|
|
}
|
|
const parts = host.split(".").filter(Boolean);
|
|
if (parts.length === 0) return `${proto}//${host}`;
|
|
// replace first label with `api`
|
|
parts[0] = "api";
|
|
return `${proto}//${parts.join(".")}`;
|
|
}
|
|
|
|
type ResolveOptions = { githubToken: string; fetchImpl: typeof fetch };
|
|
|
|
interface CachedToken {
|
|
token: string;
|
|
expiresAt: number;
|
|
updatedAt?: number;
|
|
}
|
|
|
|
function isCachedToken(value: unknown): value is CachedToken {
|
|
return (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
"token" in value &&
|
|
typeof (value as CachedToken).token === "string" &&
|
|
"expiresAt" in value &&
|
|
typeof (value as CachedToken).expiresAt === "number"
|
|
);
|
|
}
|
|
|
|
export async function resolveCopilotApiToken(opts: ResolveOptions) {
|
|
const stateDir = resolveStateDir();
|
|
const cachePath = path.join(stateDir, "github-copilot-token.json");
|
|
const now = Date.now();
|
|
|
|
try {
|
|
const cached = loadJsonFile(cachePath);
|
|
if (isCachedToken(cached) && cached.expiresAt > now) {
|
|
return {
|
|
token: cached.token,
|
|
baseUrl: deriveCopilotApiBaseUrlFromToken(cached.token),
|
|
source: `cache:${cached.updatedAt ?? "unknown"}`,
|
|
};
|
|
}
|
|
} catch {
|
|
// ignore cache read errors
|
|
}
|
|
|
|
const resp = await opts.fetchImpl("https://api.github.com/copilot/api_tokens", {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${opts.githubToken}`, Accept: "application/json" },
|
|
});
|
|
if (!resp || !resp.ok) {
|
|
throw new Error(`failed to fetch copilot token: ${resp?.status}`);
|
|
}
|
|
const body = await resp.json();
|
|
const token = String(body.token || "");
|
|
const expires_at = Number(
|
|
body.expires_at || body.expiresAt || Math.floor(Date.now() / 1000) + 3600,
|
|
);
|
|
const expiresAt = expires_at * 1000;
|
|
|
|
try {
|
|
saveJsonFile(cachePath, { token, expiresAt, updatedAt: Date.now() });
|
|
} catch {
|
|
// ignore save errors
|
|
}
|
|
|
|
return {
|
|
token,
|
|
baseUrl: deriveCopilotApiBaseUrlFromToken(token),
|
|
source: "fetched",
|
|
};
|
|
}
|
|
|
|
export default { deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken };
|