flagon-io/g1t

public

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/packages/contracts/src/identity.ts

261 lines9,968 bytesCodeBlame
1import type { RepoPath } from "./repos";
2import type { Result } from "./result";
3
4export type User = {
5 id: string;
6 username: string;
7 /**
8 * `workspace` when a workspace is acting through one of its own access
9 * tokens: `id` is then the workspace's and `username` its slug. Absent
10 * means `user`.
11 */
12 kind?: "user" | "workspace" | "agent";
13 /**
14 * Whether the account's email address is confirmed. Only set on users
15 * resolved from credentials; unverified accounts cannot change anything.
16 */
17 verified?: boolean;
18 /**
19 * The workspaces this user belongs to. Set on users resolved from
20 * credentials, so any service can authorize from it.
21 */
22 workspaces?: Membership[];
23};
24
25/** What a member may do: an owner also manages the workspace's members. */
26export type Role = "owner" | "member";
27
28export type Membership = { slug: string; role: Role };
29
30/**
31 * A workspace: the owner of repositories, and the first segment of their
32 * URLs. A person's own space and a team's are the same thing.
33 */
34export type Workspace = {
35 id: string;
36 slug: string;
37 name: string;
38 /** One line saying what the workspace is for. */
39 description: string | null;
40 /** RFC 3339. */
41 createdAt: string;
42 memberCount: number;
43};
44
45export type Member = { username: string; role: Role };
46
47/** An owner of a workspace, as staff see them. */
48export type AdminOwner = { username: string; email: string | null };
49
50/** A workspace as staff see it. Mirrors `AdminWorkspace` in `crates/contracts/src/identity.rs`. */
51export type AdminWorkspace = {
52 slug: string;
53 name: string;
54 /** RFC 3339. */
55 createdAt: string;
56 owners: AdminOwner[];
57 memberCount: number;
58};
59
60/** A member of a workspace, as staff see them. */
61export type AdminMember = { username: string; email: string | null; role: Role; /** RFC 3339. */ joined: string };
62
63export type AdminWorkspaceDetail = {
64 slug: string;
65 name: string;
66 description: string | null;
67 /** RFC 3339. */
68 createdAt: string;
69 /** Owners first, then by username. */
70 members: AdminMember[];
71};
72
73/** The most workspaces one `workspaces` call returns. */
74export const ADMIN_WORKSPACES_LIMIT = 500;
75
76/**
77 * Staff-only identity, for sudo.g1t.sh. It takes no viewer and checks no
78 * membership: only sudo calls it, over its service binding, once Cloudflare
79 * Access and its staff list have let someone in. Never call it on behalf of
80 * a customer.
81 */
82export interface IdentityAdminApi {
83 /** Every workspace, newest first, at most 500; `query` matches slug, name, or an owner's username or email. */
84 workspaces(query?: string): Promise<AdminWorkspace[]>;
85 /** One workspace with all its members, or null. */
86 workspace(slug: string): Promise<AdminWorkspaceDetail | null>;
87}
88
89/** Who is asking. Every read and write in every service takes one. */
90export type Viewer = User | null;
91
92export type SshKey = {
93 id: string;
94 title: string;
95 fingerprint: string;
96 /** RFC 3339. */
97 createdAt: string;
98};
99
100export type AccessToken = {
101 id: string;
102 name: string;
103 /** RFC 3339. */
104 createdAt: string;
105 /** RFC 3339, to within a few minutes. Null until it is first used. */
106 lastUsedAt: string | null;
107 /**
108 * For a workspace's token, the username of the member who made it. Null
109 * once that account is gone, and on personal tokens.
110 */
111 createdBy: string | null;
112};
113
114export type DeviceStart = {
115 /** Secret held by the tool and exchanged for a token once approved. */
116 deviceCode: string;
117 /** Short code shown to the person, e.g. `WDJB-MJHT`. */
118 userCode: string;
119 /** Seconds until both codes stop working. */
120 expiresIn: number;
121 /** Seconds the tool should wait between polls. */
122 interval: number;
123};
124
125export type DeviceRequest = { userCode: string; clientName: string };
126
127export type DeviceClaim =
128 | { status: "pending" | "denied" | "expired" }
129 | { status: "approved"; token: string; user: User };
130
131/** What the site passes on once a person has approved an application. */
132export type OAuthApproval = {
133 clientId: string;
134 /** Shown wherever the application's access is listed. */
135 clientName: string;
136 redirectUri: string;
137 /** PKCE challenge, method S256. */
138 codeChallenge: string;
139};
140
141export type OAuthTokens = {
142 accessToken: string;
143 /** Works once; using it returns the next one. */
144 refreshToken: string;
145 /** Seconds until the access token stops working. */
146 expiresIn: number;
147};
148
149/** An application a person has signed in to. */
150export type OAuthGrant = {
151 id: string;
152 clientName: string;
153 /** RFC 3339. */
154 createdAt: string;
155 /** RFC 3339. */
156 lastUsedAt: string;
157};
158
159/** Accounts, credentials and sessions. */
160export interface IdentityApi {
161 /** Creates an account and signs it in. */
162 register(username: string, email: string, password: string): Promise<Result<{ user: User; sessionToken: string }>>;
163 /** Verifies a username and password for website sign-in. */
164 signIn(username: string, password: string): Promise<Result<{ user: User; sessionToken: string }>>;
165 signOut(sessionToken: string): Promise<void>;
166
167 /** Sends the confirmation email again. */
168 resendVerification(user: User): Promise<Result<boolean>>;
169 /** Confirms the address the emailed token was sent to. */
170 verifyEmail(token: string): Promise<Result<User>>;
171 /** Emails a reset link if the address has an account. Always resolves. */
172 requestPasswordReset(email: string): Promise<boolean>;
173 /** Sets a new password from an emailed token and ends every session. */
174 resetPassword(token: string, password: string): Promise<Result<User>>;
175
176 /**
177 * Device sign-in (RFC 8628). A tool starts a request, a person approves
178 * its short code in a browser, and the tool claims an access token.
179 */
180 deviceStart(clientName: string): Promise<DeviceStart>;
181 /** What a user code is asking for, or null if it is not valid. */
182 deviceLookup(userCode: string): Promise<DeviceRequest | null>;
183 deviceResolve(userCode: string, user: User, approve: boolean): Promise<Result<boolean>>;
184 deviceClaim(deviceCode: string): Promise<DeviceClaim>;
185
186 /**
187 * OAuth 2.1 for applications that sign a person in through the browser.
188 * The caller has checked the client and its redirect address; this
189 * returns the one-time code the application exchanges for tokens.
190 */
191 oauthAuthorize(user: User, approval: OAuthApproval): Promise<{ code: string }>;
192 /** Redeems a code. It works once, for that client, with the PKCE verifier. */
193 oauthExchange(code: string, codeVerifier: string, clientId: string, redirectUri: string): Promise<Result<OAuthTokens>>;
194 /** Trades a refresh token for new tokens; the old ones stop working. */
195 oauthRefresh(refreshToken: string, clientId: string): Promise<Result<OAuthTokens>>;
196 /** Applications the user has signed in to, most recently used first. */
197 listOAuthGrants(user: User): Promise<OAuthGrant[]>;
198 /** Signs an application out. */
199 revokeOAuthGrant(user: User, id: string): Promise<void>;
200
201 createWorkspace(user: User, slug: string, name: string): Promise<Result<Workspace>>;
202 /** Public details of a workspace, or null. */
203 getWorkspace(slug: string): Promise<Workspace | null>;
204 /** Members only. */
205 listMembers(slug: string, viewer: Viewer): Promise<Result<Member[]>>;
206 /** Owners only. */
207 addMember(actor: User, slug: string, username: string): Promise<Result<boolean>>;
208 /** Owners only. */
209 removeMember(actor: User, slug: string, username: string): Promise<Result<boolean>>;
210 /** Owners only. An empty name falls back to the slug. */
211 updateWorkspace(actor: User, slug: string, details: { name: string; description: string }): Promise<Result<Workspace>>;
212
213 /**
214 * A workspace's own access tokens. They belong to the workspace, act as
215 * it, and keep working when the member who made one leaves. Members only.
216 */
217 listWorkspaceTokens(slug: string, viewer: Viewer): Promise<Result<AccessToken[]>>;
218 /** Owners only. The plaintext token is returned once and never stored. */
219 createWorkspaceToken(actor: User, slug: string, name: string): Promise<Result<{ token: string; info: AccessToken }>>;
220 /** Owners only. */
221 removeWorkspaceToken(actor: User, slug: string, id: string): Promise<Result<boolean>>;
222
223 userForSession(sessionToken: string): Promise<Viewer>;
224
225 /** Verifies git credentials: the account password or an access token. */
226 userForGitCredentials(username: string, secret: string): Promise<Viewer>;
227 /** Resolves a `g1t_…` access token, as sent to the API and MCP server. */
228 userForAccessToken(token: string): Promise<Viewer>;
229 userForSshKey(fingerprint: string): Promise<Viewer>;
230 userByUsername(username: string): Promise<Viewer>;
231 /** The names behind account and workspace ids; unknown ids are left out. */
232 usernames(ids: string[]): Promise<Record<string, string>>;
233
234 listSshKeys(user: User): Promise<SshKey[]>;
235 /** Takes one line in OpenSSH public key format. */
236 addSshKey(user: User, title: string, publicKey: string): Promise<Result<SshKey>>;
237 removeSshKey(user: User, id: string): Promise<void>;
238
239 listAccessTokens(user: User): Promise<AccessToken[]>;
240 /**
241 * The plaintext token is returned once and never stored. With
242 * `ttlSeconds` the token expires and is left out of token lists; that
243 * form is used for hosted agents. A token made for a workspace acting
244 * through a token of its own belongs to that workspace too.
245 */
246 createAccessToken(user: User, name: string, ttlSeconds?: number): Promise<{ token: string; info: AccessToken }>;
247 /**
248 * A token for a g1t agent working for `onBehalfOf`: it acts as
249 * `g1t-agent`, in `scope.repo` only, and only for `scope.operations`.
250 */
251 createAgentToken(
252 onBehalfOf: User,
253 scope: AgentScope,
254 ttlSeconds: number,
255 ): Promise<{ token: string; info: AccessToken }>;
256 removeAccessToken(user: User, id: string): Promise<void>;
257}
258
259
260/** What an agent's token may do: these operations, in this repository. */
261export type AgentScope = { repo: RepoPath; operations: string[] };