1import type { GitHubSettings } from "../client/definition.ts";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5const GitHubAuthStatusAccountSchema = Schema.Struct({
6 state: Schema.String,
7 error: Schema.optional(Schema.String),
8 active: Schema.Boolean,
9 host: Schema.String,
10 login: Schema.String,
11 tokenSource: Schema.optional(Schema.String),
12});
13
14const GitHubAuthStatusSchema = Schema.Struct({
15 hosts: Schema.Record(Schema.String, Schema.Array(GitHubAuthStatusAccountSchema)),
16});
17
18const decodeGitHubAuthStatusJson = Schema.decodeUnknownOption(
19 Schema.fromJsonString(GitHubAuthStatusSchema),
20);
21
22export interface GitHubAuthStatusAccount {
23 readonly host: string;
24 readonly account: string;
25 readonly authenticated: boolean;
26 readonly active: boolean;
27 readonly error: string | null;
28 /** The variable (GH_TOKEN and kin) the login came from; it overrides every stored login. */
29 readonly environmentVariable: string | null;
30}
31
32export interface GitHubAuthStatus {
33 readonly parsed: boolean;
34 readonly accounts: ReadonlyArray<GitHubAuthStatusAccount>;
35}
36
37const ENVIRONMENT_TOKEN_SOURCE = /^(?:GH|GITHUB)_(?:ENTERPRISE_)?TOKEN$/u;
38
39function nonEmptyString(value: string): string | null {
40 const trimmed = value.trim();
41 return trimmed.length > 0 ? trimmed : null;
42}
43
44export function parseGitHubAuthStatus(text: string): GitHubAuthStatus {
45 return Option.match(decodeGitHubAuthStatusJson(text), {
46 onNone: () => ({ parsed: false, accounts: [] }),
47 onSome: (status) =>
48 ({
49 parsed: true,
50 accounts: Object.values(status.hosts).flatMap((accounts) =>
51 accounts.flatMap((account) => {
52 const host = nonEmptyString(account.host);
53 const login = nonEmptyString(account.login);
54 if (host === null || login === null) return [];
55
56 return [
57 {
58 host: host.toLowerCase(),
59 account: login,
60 authenticated: account.state === "success",
61 active: account.active,
62 error: account.error?.trim() || null,
63 environmentVariable: ENVIRONMENT_TOKEN_SOURCE.test(account.tokenSource ?? "")
64 ? (account.tokenSource ?? null)
65 : null,
66 },
67 ];
68 }),
69 ),
70 }) satisfies GitHubAuthStatus,
71 });
72}
73
74export function findAuthenticatedGitHubAccount(
75 accounts: ReadonlyArray<GitHubAuthStatusAccount>,
76): GitHubAuthStatusAccount | undefined {
77 return (
78 accounts.find((account) => account.authenticated && account.active) ??
79 accounts.find((account) => account.authenticated)
80 );
81}
82
83/**
84 * The login GitHub requests for a host will use, honoring Settings: an environment token
85 * first (gh's own precedence), then the pinned account, then gh's active login.
86 * Returns undefined when the host is turned off or has no usable login.
87 */
88export function effectiveGitHubAccount(
89 host: string,
90 accounts: ReadonlyArray<GitHubAuthStatusAccount>,
91 settings: GitHubSettings,
92): GitHubAuthStatusAccount | undefined {
93 const choice = settings.hosts[host];
94 if (choice?.enabled === false) return undefined;
95 const usable = accounts.filter((account) => account.host === host && account.authenticated);
96 return (
97 usable.find((account) => account.environmentVariable !== null) ??
98 (choice?.account === undefined
99 ? undefined
100 : usable.find((account) => account.account === choice.account)) ??
101 findAuthenticatedGitHubAccount(usable)
102 );
103}