import { Octokit } from "@octokit/rest" import * as z from "zod" import { parseRetryAfter, retryableActionError, terminalActionError, } from "../../../automation/actions" const GITHUB_SECRET_SCHEMA = z.object({ accessToken: z.string().min(1), }) /** * Creates an authenticated Octokit client for custom GitHub API calls. * * @param secret - Runtime GitHub account secret. */ export function getGitHubApi(secret: Record): Octokit { const api = new Octokit({ auth: getGitHubAccessToken(secret), userAgent: "Automate.ax", }) api.hook.before("request", (options) => { options.headers["x-github-api-version"] = "2026-03-10" }) api.hook.error("request", (error) => { const response = githubErrorResponse(error) if (!response) throw error const retryAt = githubRetryAt(response.headers) if ( response.status === 429 || (response.status === 403 && (response.headers["retry-after"] !== undefined || response.headers["x-ratelimit-remaining"] === "0")) ) { throw retryableActionError(error, { retryAt }) } if (response.status < 500) throw terminalActionError(error) throw error }) return api } /** * Extracts the response metadata Octokit attaches to HTTP errors. * * @param error - Octokit request error. */ function githubErrorResponse(error: Error) { if (!("response" in error) || !isRecord(error.response)) return undefined const { headers, status } = error.response if (!isRecord(headers) || typeof status !== "number") return undefined return { headers: normalizeHeaders(headers), status } } /** * Resolves GitHub's Retry-After or primary-rate-limit reset timestamp. * * @param headers - Normalized GitHub response headers. */ function githubRetryAt(headers: Record) { const retryAt = parseRetryAfter(headers["retry-after"]) if (retryAt) return retryAt if (headers["x-ratelimit-remaining"] !== "0") return undefined const reset = headers["x-ratelimit-reset"] if (reset === undefined || !/^\d+$/.test(reset)) return undefined const value = new Date(Number(reset) * 1_000) return value.getTime() > Date.now() ? value : undefined } /** * Normalizes Octokit's response headers for case-insensitive lookup. * * @param headers - Raw Octokit response headers. */ function normalizeHeaders(headers: Record) { return Object.fromEntries( Object.entries(headers).flatMap(([key, value]) => typeof value === "string" ? [[key.toLowerCase(), value]] : [], ), ) } /** * Checks one unknown value for an ordinary object record. * * @param value - Candidate record. */ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } /** * Returns the installation token for provider calls Octokit cannot encode. * * @param secret - Runtime GitHub account secret. */ export function getGitHubAccessToken(secret: Record) { return GITHUB_SECRET_SCHEMA.parse(secret).accessToken }