Skip to content
175 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 look1import type { ServiceBinding } from "./clients";
2import type { User } from "./identity";
3import 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
15export type GithubPurpose = "sign_in" | "link";
16
17/** How a return from GitHub ended. */
18export type GithubFinished =
Merge main (membership, two-factor, GitHub repo roles) into tokens19 | {
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 look26 | { 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
32export type GithubPending = {
33 login: string;
34 kind: "link" | "username";
35 suggestion: string | null;
36 next: string;
37 inviteRequired: boolean;
38};
39
40export 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
48export type GithubAccountView = {
49 enabled: boolean;
50 account: GithubAccount | null;
51 hasPassword: boolean;
52};
53
54/** How a GitHub repository comes to g1t. */
55export type GithubMode = "import" | "mirror" | "push";
56
57export 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 */
73export 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 look84export type GithubAppStatus = {
85 configured: boolean;
86 installUrl: string | null;
87 linked: boolean;
88 installations: GithubInstallation[];
89};
90
91export 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
102export type GithubRepositories = {
103 repositories: GithubRepository[];
104 total: number;
105 page: number;
106 perPage: number;
107};
108
109export 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
121export type GithubImportInput = {
122 installationId: number;
123 githubRepoId: number;
124 name?: string;
125 mode: GithubMode;
126 private?: boolean;
127 issues: boolean;
128};
129
130async 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. */
141export 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. */
158export 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 look165 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.