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 | import type { ServiceBinding } from "./clients"; |
| 2 | import type { User } from "./identity"; | |
| 3 | import type { Result } from "./result"; | |
| 4 | ||
| 5 | /** | |
| 6 | * GitHub: signing in with a GitHub account, and bringing repositories | |
| 7 | * across through g1t's GitHub App. Mirrors `crates/contracts/src/github.rs`. | |
| 8 | * | |
| 9 | * The identity service keeps linked accounts and their user tokens; the | |
| 10 | * integrations service keeps installations, linked repositories and the | |
| 11 | * app's webhook. With no app configured, `enabled`/`configured` are false | |
| 12 | * and the site shows no GitHub buttons. | |
| 13 | */ | |
| 14 | ||
| 15 | export type GithubPurpose = "sign_in" | "link"; | |
| 16 | ||
| 17 | /** How a return from GitHub ended. */ | |
| 18 | export type GithubFinished = | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 19 | | { |
| 20 | kind: "signed_in"; | |
| 21 | /** `twoFactorChallenge` is set, and `sessionToken` empty, while the account's two-factor code is still to come. */ | |
| 22 | signedIn: { user: User; sessionToken: string; twoFactorChallenge?: string | null }; | |
| 23 | created: boolean; | |
| 24 | next: string; | |
| 25 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 26 | | { kind: "linked"; login: string; next: string } |
| 27 | /** An account has one of its verified emails: sign in to it, then claim. */ | |
| 28 | | { kind: "needs_link"; pending: string; login: string; next: string } | |
| 29 | /** A new account waiting on a username, or on an invite code. */ | |
| 30 | | { kind: "needs_username"; pending: string; login: string; suggestion: string; next: string; inviteRequired: boolean }; | |
| 31 | ||
| 32 | export type GithubPending = { | |
| 33 | login: string; | |
| 34 | kind: "link" | "username"; | |
| 35 | suggestion: string | null; | |
| 36 | next: string; | |
| 37 | inviteRequired: boolean; | |
| 38 | }; | |
| 39 | ||
| 40 | export type GithubAccount = { | |
| 41 | githubId: number; | |
| 42 | login: string; | |
| 43 | linkedAt: string; | |
| 44 | /** Whether g1t holds a working user token for it. */ | |
| 45 | authorized: boolean; | |
| 46 | }; | |
| 47 | ||
| 48 | export type GithubAccountView = { | |
| 49 | enabled: boolean; | |
| 50 | account: GithubAccount | null; | |
| 51 | hasPassword: boolean; | |
| 52 | }; | |
| 53 | ||
| 54 | /** How a GitHub repository comes to g1t. */ | |
| 55 | export type GithubMode = "import" | "mirror" | "push"; | |
| 56 | ||
| 57 | export type GithubInstallation = { | |
| 58 | id: number; | |
| 59 | workspace: string; | |
| 60 | account: string; | |
| 61 | accountType: string; | |
| 62 | repositorySelection: "all" | "selected" | string; | |
| 63 | suspended: boolean; | |
| 64 | settingsUrl: string; | |
| 65 | createdAt: string; | |
| 66 | }; | |
| 67 | ||
| 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. | 68 | /** |
| 69 | * An installation of the app the person's own GitHub account can see, | |
| 70 | * from `github_visible_installations`: how one installed on GitHub | |
| 71 | * directly is found and added to a workspace. | |
| 72 | */ | |
| 73 | export type GithubVisibleInstallation = { | |
| 74 | id: number; | |
| 75 | account: string; | |
| 76 | accountType: string; | |
| 77 | repositorySelection: "all" | "selected" | string; | |
| 78 | suspended: boolean; | |
| 79 | settingsUrl: string; | |
| 80 | /** The person's workspaces it is recorded in already. */ | |
| 81 | recordedIn: string[]; | |
| 82 | }; | |
| 83 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 84 | export type GithubAppStatus = { |
| 85 | configured: boolean; | |
| 86 | installUrl: string | null; | |
| 87 | linked: boolean; | |
| 88 | installations: GithubInstallation[]; | |
| 89 | }; | |
| 90 | ||
| 91 | export type GithubRepository = { | |
| 92 | id: number; | |
| 93 | fullName: string; | |
| 94 | name: string; | |
| 95 | private: boolean; | |
| 96 | description: string | null; | |
| 97 | defaultBranch: string; | |
| 98 | /** The g1t repository already linked to it, as `workspace/name`. */ | |
| 99 | linkedTo: string | null; | |
| 100 | }; | |
| 101 | ||
| 102 | export type GithubRepositories = { | |
| 103 | repositories: GithubRepository[]; | |
| 104 | total: number; | |
| 105 | page: number; | |
| 106 | perPage: number; | |
| 107 | }; | |
| 108 | ||
| 109 | export type GithubRepoLink = { | |
| 110 | repoId: string; | |
| 111 | repo: string; | |
| 112 | installationId: number; | |
| 113 | githubRepoId: number; | |
| 114 | fullName: string; | |
| 115 | mode: GithubMode; | |
| 116 | syncedAt: string | null; | |
| 117 | lastError: string | null; | |
| 118 | issuesImported: number; | |
| 119 | }; | |
| 120 | ||
| 121 | export type GithubImportInput = { | |
| 122 | installationId: number; | |
| 123 | githubRepoId: number; | |
| 124 | name?: string; | |
| 125 | mode: GithubMode; | |
| 126 | private?: boolean; | |
| 127 | issues: boolean; | |
| 128 | }; | |
| 129 | ||
| 130 | async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> { | |
| 131 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 132 | method: "POST", | |
| 133 | headers: { "content-type": "application/json" }, | |
| 134 | body: JSON.stringify(args), | |
| 135 | }); | |
| 136 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 137 | return (await response.json()) as T; | |
| 138 | } | |
| 139 | ||
| 140 | /** Signing in with GitHub: methods of the identity service. */ | |
| 141 | export function githubSignInClient(identity: ServiceBinding) { | |
| 142 | const call = <T>(method: string, args: object) => rpc<T>(identity, method, args); | |
| 143 | return { | |
| 144 | enabled: () => call<boolean>("github_enabled", {}), | |
| 145 | start: (input: { purpose: GithubPurpose; user?: User | null; redirectUri: string; next: string; inviteCode?: string | null }) => | |
| 146 | call<Result<{ authorizeUrl: string; state: string }>>("github_start", input), | |
| 147 | finish: (state: string, code: string) => call<Result<GithubFinished>>("github_finish", { state, code }), | |
| 148 | pending: (pending: string) => call<Result<GithubPending>>("github_pending", { pending }), | |
| 149 | signUp: (pending: string, username: string, inviteCode?: string | null) => | |
| 150 | call<Result<{ user: User; sessionToken: string }>>("github_sign_up", { pending, username, inviteCode: inviteCode ?? null }), | |
| 151 | claim: (pending: string, user: User) => call<Result<GithubAccount>>("github_claim", { pending, user }), | |
| 152 | account: (user: User) => call<GithubAccountView>("github_account", { user }), | |
| 153 | unlink: (user: User) => call<Result<boolean>>("github_unlink", { user }), | |
| 154 | }; | |
| 155 | } | |
| 156 | ||
| 157 | /** g1t's GitHub App: methods of the integrations service. */ | |
| 158 | export function githubAppClient(integrations: ServiceBinding) { | |
| 159 | const call = <T>(method: string, args: object) => rpc<T>(integrations, method, args); | |
| 160 | return { | |
| 161 | status: (viewer: User, workspace: string) => call<Result<GithubAppStatus>>("github_status", { viewer, workspace }), | |
| 162 | addInstallation: (actor: User, workspace: string, installationId: number) => | |
| 163 | call<Result<GithubInstallation>>("github_add_installation", { actor, workspace, installationId }), | |
| 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. | 164 | visibleInstallations: (actor: User) => call<Result<GithubVisibleInstallation[]>>("github_visible_installations", { actor }), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 165 | removeInstallation: (actor: User, workspace: string, installationId: number) => |
| 166 | call<Result<boolean>>("github_remove_installation", { actor, workspace, installationId }), | |
| 167 | repositories: (actor: User, workspace: string, installationId: number, page?: number) => | |
| 168 | call<Result<GithubRepositories>>("github_repositories", { actor, workspace, installationId, page: page ?? null }), | |
| 169 | import: (actor: User, workspace: string, input: GithubImportInput) => | |
| 170 | call<Result<GithubRepoLink>>("github_import", { actor, workspace, ...input }), | |
| 171 | link: (repoId: string) => call<GithubRepoLink | null>("github_link", { repoId }), | |
| 172 | unlinkRepo: (actor: User, repoId: string) => call<Result<boolean>>("github_unlink_repo", { actor, repoId }), | |
| 173 | sync: (actor: User, repoId: string) => call<Result<GithubRepoLink>>("github_sync", { actor, repoId }), | |
| 174 | }; | |
| 175 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.