1import * as Effect from "effect/Effect";
2import { AuthError } from "../Auth/AuthProvider.ts";
3import { getEnv } from "../Auth/Env.ts";
4
5/**
6 * Normalize a user-supplied GitHub host or URL into the REST API base URL
7 * Octokit expects, following the same conventions as the `gh` CLI and the
8 * Terraform GitHub provider:
9 *
10 * - `github.com` / `api.github.com` → `undefined` (Octokit's default)
11 * - GitHub Enterprise Cloud with data residency (`acme.ghe.com` or
12 * `api.acme.ghe.com`) → `https://api.acme.ghe.com`
13 * - GitHub Enterprise Server (`github.example.com`) → `https://github.example.com/api/v3`;
14 * an explicit path (e.g. an already-complete `/api/v3` URL) is honored as-is.
15 *
16 * Accepts a bare hostname or a full URL.
17 */
18export const normalizeGitHubBaseUrl = (
19 input: string,
20): Effect.Effect<string | undefined, AuthError> =>
21 Effect.try({
22 try: () => {
23 const trimmed = input.trim();
24 const url = new URL(
25 trimmed.includes("://") ? trimmed : `https://${trimmed}`,
26 );
27 const host = url.hostname.toLowerCase();
28 if (
29 host === "github.com" ||
30 host === "www.github.com" ||
31 host === "api.github.com"
32 ) {
33 return undefined;
34 }
35 if (host.endsWith(".ghe.com")) {
36 return `https://${host.startsWith("api.") ? host : `api.${host}`}`;
37 }
38 const path = url.pathname.replace(/\/+$/, "");
39 return `${url.protocol}//${url.host}${path === "" ? "/api/v3" : path}`;
40 },
41 catch: () =>
42 new AuthError({
43 message: `Invalid GitHub base URL: '${input}'. Provide a hostname (github.example.com) or URL (https://github.example.com/api/v3).`,
44 }),
45 });
46
47/**
48 * The hostname `gh auth token --hostname` expects for a normalized API base
49 * URL — the plain host for GitHub Enterprise Server, and the `api.`-less
50 * host for GitHub Enterprise Cloud with data residency.
51 */
52export const githubHostname = (baseUrl: string): string => {
53 const host = new URL(baseUrl).hostname;
54 return host.startsWith("api.") && host.endsWith(".ghe.com")
55 ? host.slice("api.".length)
56 : host;
57};
58
59/**
60 * Resolve the GitHub API base URL from the environment:
61 * `GITHUB_BASE_URL` (Terraform convention), then `GITHUB_API_URL` (set by
62 * GitHub Actions runners), then `GH_HOST` (gh CLI convention, a bare
63 * hostname). Returns `undefined` when unset or when the value points at
64 * github.com.
65 */
66export const resolveGitHubBaseUrlFromEnv: Effect.Effect<
67 string | undefined,
68 AuthError
69> = Effect.gen(function* () {
70 for (const key of ["GITHUB_BASE_URL", "GITHUB_API_URL", "GH_HOST"]) {
71 const value = yield* getEnv(key);
72 if (value !== undefined && value.trim() !== "") {
73 return yield* normalizeGitHubBaseUrl(value);
74 }
75 }
76 return undefined;
77});