openclaw/src/providers/github-copilot-token.ts

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 };