Skip to content
151 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1/**
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";
Merge main (membership, two-factor, GitHub repo roles) into tokens11/** 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;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15/** 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
A GitHub App installed straight on GitHub can be added to a workspace: Import from GitHub lists the installations your GitHub account can see that this workspace hasn't added, each with Add for owners, and coming back from GitHub without g1t's own start asks which of your workspaces to add it to instead of stopping; the GitHub guide says how.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/** How an installation is described in a list: `Organization · all repositories`. */
95export function installationSummary(item: { accountType: string; repositorySelection: string }): string {
96 const kind = item.accountType === "Organization" ? "Organization" : "Personal";
97 return `${kind} · ${item.repositorySelection === "all" ? "all repositories" : "selected repositories"}`;
98}
99
100/**
101 * The workspaces an installation can be added to from the setup page: the
102 * ones the person owns, each saying whether it has the installation already.
103 */
104export function setupChoices(
105 workspaces: { slug: string; name?: string | null; role: string }[],
106 installation: SeenInstallation | null,
107): { slug: string; name: string; added: boolean }[] {
108 return workspaces
109 .filter((membership) => membership.role === "owner")
110 .map((membership) => ({
111 slug: membership.slug,
112 name: membership.name ?? membership.slug,
113 added: installation?.recordedIn.includes(membership.slug) ?? false,
114 }));
115}
116
117/**
118 * What the Integrations directory and the Marketplace say about GitHub
119 * from the installations a workspace has recorded: nothing until there is
120 * one, then the accounts it is on and any that GitHub has suspended.
121 */
122export function githubConnected(
123 installations: { account: string; suspended: boolean }[],
124): { detail: string; problem: string | null; manage: null } | null {
125 if (installations.length === 0) return null;
126 const suspended = installations.find((installation) => installation.suspended);
127 return {
128 detail: `On ${installations.map((installation) => installation.account).join(", ")}`,
129 problem: suspended ? `The installation on ${suspended.account} is suspended on GitHub.` : null,
130 manage: null,
131 };
132}
133
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look134/** What each way of bringing a repository across does, for the picker. */
135export const MODES = [
136 {
137 id: "import",
138 title: "Import",
139 text: "Copy it once: every branch and tag, and its issues if you like. The copy on g1t is then its own.",
140 },
141 {
142 id: "mirror",
Merge branch 'mirroring' into artifacts-mode143 title: "Standby mirror",
144 text: "g1t keeps a read-only copy that follows GitHub. Take over whenever you need to work here.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look145 },
146 {
147 id: "push",
148 title: "Move to g1t",
Merge branch 'mirroring' into artifacts-mode149 text: "g1t leads; GitHub follows every push.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look150 },
151] as const;

This file's history is long; its oldest lines are credited to the oldest commit read.