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 look | 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. */ | |
| 8 | export const STATE_COOKIE = "g1t_github_state"; | |
| 9 | /** A GitHub sign-in waiting on a username, or on signing in to link. */ | |
| 10 | export const PENDING_COOKIE = "g1t_github_pending"; | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 11 | /** A sign-in that gave the right password and waits for a two-factor code: identity's challenge. */ |
| 12 | export const TWO_FACTOR_COOKIE = "g1t_two_factor"; | |
| 13 | /** How long that waits, in seconds, as identity's `TWO_FACTOR_CHALLENGE_SECONDS`. */ | |
| 14 | export const TWO_FACTOR_SECONDS = 600; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | /** An installation under way: its state and the workspace it is for. */ |
| 16 | export const INSTALL_COOKIE = "g1t_github_install"; | |
| 17 | ||
| 18 | /** The value of a cookie, or null. Values here are hex and slugs only. */ | |
| 19 | export 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. */ | |
| 31 | export 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. */ | |
| 36 | export 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 | */ | |
| 48 | export 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>`. */ | |
| 54 | export 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. */ | |
| 59 | export 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. */ | |
| 69 | export 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. */ |
| 76 | export 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 | */ | |
| 90 | export 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`. */ | |
| 95 | export 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 | */ | |
| 104 | export 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 | */ | |
| 122 | export 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 look | 134 | /** What each way of bringing a repository across does, for the picker. */ |
| 135 | export 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-mode | 143 | 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 look | 145 | }, |
| 146 | { | |
| 147 | id: "push", | |
| 148 | title: "Move to g1t", | |
| Merge branch 'mirroring' into artifacts-mode | 149 | text: "g1t leads; GitHub follows every push.", |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 150 | }, |
| 151 | ] as const; |
This file's history is long; its oldest lines are credited to the oldest commit read.