Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
160 linesCodeBlameRaw
1/**
2 * The parts of signing in with GitHub and installing g1t's GitHub App that
3 * the site itself decides: the cookies that bind a trip to GitHub to the
4 * browser that started it, and checking what comes back.
5 */
6
7/** The state sent to GitHub to sign in or link, for ten minutes. */
8export const STATE_COOKIE = "g1t_github_state";
9/** A GitHub sign-in waiting on a username, or on signing in to link. */
10export const PENDING_COOKIE = "g1t_github_pending";
11/** A sign-in that gave the right password and waits for a two-factor code: identity's challenge. */
12export const TWO_FACTOR_COOKIE = "g1t_two_factor";
13/** How long that waits, in seconds, as identity's `TWO_FACTOR_CHALLENGE_SECONDS`. */
14export const TWO_FACTOR_SECONDS = 600;
15/** An installation under way: its state and the workspace it is for. */
16export const INSTALL_COOKIE = "g1t_github_install";
17
18/** The value of a cookie, or null. Values here are hex and slugs only. */
19export function readCookie(header: string | null, name: string): string | null {
20 for (const part of (header ?? "").split(";")) {
21 const [key, ...rest] = part.trim().split("=");
22 if (key === name) {
23 const value = rest.join("=");
24 return /^[0-9a-z.-]{1,200}$/.test(value) ? value : null;
25 }
26 }
27 return null;
28}
29
30/** A `Set-Cookie` value: HttpOnly, Secure, same-site Lax; 0 clears it. */
31export function cookie(name: string, value: string, maxAge: number): string {
32 return `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
33}
34
35/** Compares two strings in time that does not depend on where they differ. */
36export function sameString(a: string, b: string): boolean {
37 if (a.length !== b.length) return false;
38 let difference = 0;
39 for (let i = 0; i < a.length; i++) difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
40 return difference === 0;
41}
42
43/**
44 * Whether the state GitHub sent back is the one this browser was given.
45 * Both must be present: a callback without the cookie came from somewhere
46 * else.
47 */
48export function stateMatches(fromCookie: string | null, fromGithub: string | null): boolean {
49 if (!fromCookie || !fromGithub) return false;
50 return sameString(fromCookie, fromGithub);
51}
52
53/** The install cookie's value: `<state>.<workspace>`. */
54export function installCookieValue(state: string, workspace: string): string {
55 return `${state}.${workspace}`;
56}
57
58/** The workspace an installation was started for, if the state matches. */
59export function installWorkspace(fromCookie: string | null, fromGithub: string | null): string | null {
60 if (!fromCookie) return null;
61 const dot = fromCookie.indexOf(".");
62 if (dot < 0) return null;
63 const state = fromCookie.slice(0, dot);
64 const workspace = fromCookie.slice(dot + 1);
65 return stateMatches(state, fromGithub) && workspace ? workspace : null;
66}
67
68/** A fresh random state: 32 bytes, hex. */
69export function newState(): string {
70 const bytes = new Uint8Array(32);
71 crypto.getRandomValues(bytes);
72 return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
73}
74
75/** An installation as GitHub lists it to the person, with the workspaces of theirs that have it. */
76export type SeenInstallation = {
77 id: number;
78 account: string;
79 accountType: string;
80 repositorySelection: string;
81 suspended: boolean;
82 recordedIn: string[];
83};
84
85/**
86 * The installations the person can see on GitHub that `workspace` has not
87 * added yet: the app installed on GitHub directly, or from a return that
88 * lost g1t's state.
89 */
90export function notYetAdded<T extends SeenInstallation>(seen: T[], workspace: string): T[] {
91 return seen.filter((item) => !item.recordedIn.includes(workspace));
92}
93
94/** Where "Connect GitHub" goes next for a workspace owner. */
95export type ConnectStep =
96 | { kind: "link" }
97 | { kind: "add"; installation: number }
98 | { kind: "choose" }
99 | { kind: "install" };
100
101/**
102 * One click connects GitHub to a workspace: the person's GitHub account is
103 * linked first (g1t lists repositories as them), an installation they
104 * already have is added rather than installed again, several are left to
105 * choose from, and only with none is the app installed on GitHub.
106 * `another` is an owner asking to install on one more account; `justLinked`
107 * stops a link that did not take from sending them round again.
108 */
109export function connectStep(input: {
110 linked: boolean;
111 justLinked: boolean;
112 another: boolean;
113 unadded: { id: number; suspended: boolean }[];
114}): ConnectStep {
115 if (!input.linked) return input.justLinked ? { kind: "choose" } : { kind: "link" };
116 if (input.another) return { kind: "install" };
117 const usable = input.unadded.filter((item) => !item.suspended);
118 if (usable.length === 1 && input.unadded.length === 1) return { kind: "add", installation: usable[0].id };
119 return input.unadded.length > 0 ? { kind: "choose" } : { kind: "install" };
120}
121
122/** How an installation is described in a list: `Organization · all repositories`. */
123export function installationSummary(item: { accountType: string; repositorySelection: string }): string {
124 const kind = item.accountType === "Organization" ? "Organization" : "Personal";
125 return `${kind} · ${item.repositorySelection === "all" ? "all repositories" : "selected repositories"}`;
126}
127
128/**
129 * The workspaces an installation can be added to from the setup page: the
130 * ones the person owns, each saying whether it has the installation already.
131 */
132export function setupChoices(
133 workspaces: { slug: string; name?: string | null; role: string }[],
134 installation: SeenInstallation | null,
135): { slug: string; name: string; added: boolean }[] {
136 return workspaces
137 .filter((membership) => membership.role === "owner")
138 .map((membership) => ({
139 slug: membership.slug,
140 name: membership.name ?? membership.slug,
141 added: installation?.recordedIn.includes(membership.slug) ?? false,
142 }));
143}
144
145/**
146 * What the Integrations directory and Apps say about GitHub
147 * from the installations a workspace has recorded: nothing until there is
148 * one, then the accounts it is on and any that GitHub has suspended.
149 */
150export function githubConnected(
151 installations: { account: string; suspended: boolean }[],
152): { detail: string; problem: string | null; manage: null } | null {
153 if (installations.length === 0) return null;
154 const suspended = installations.find((installation) => installation.suspended);
155 return {
156 detail: `On ${installations.map((installation) => installation.account).join(", ")}`,
157 problem: suspended ? `The installation on ${suspended.account} is suspended on GitHub.` : null,
158 manage: null,
159 };
160}