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/crates/contracts/src/identity.rs

659 lines20,705 bytesCodeBlame
1//! The identity service: accounts, credentials and sessions.
2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::User;
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SshKey {
13 pub id: String,
14 pub title: String,
15 pub fingerprint: String,
16 /// RFC 3339.
17 pub created_at: String,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct AccessToken {
23 pub id: String,
24 pub name: String,
25 /// RFC 3339.
26 pub created_at: String,
27 /// RFC 3339, to within a few minutes. Null until it is first used.
28 pub last_used_at: Option<String>,
29 /// For a workspace's token, the username of the member who made it.
30 /// Null once that account is gone, and on personal tokens.
31 pub created_by: Option<String>,
32}
33
34/// `sign_in`: verifies a username and password for website sign-in.
35/// Returns `Outcome<SignedIn>`.
36#[derive(Debug, Serialize, Deserialize)]
37pub struct SignInArgs {
38 pub username: String,
39 pub password: String,
40}
41
42#[derive(Debug, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct SignedIn {
45 pub user: User,
46 pub session_token: String,
47}
48
49/// `sign_out` and `user_for_session`.
50#[derive(Debug, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SessionArgs {
53 pub session_token: String,
54}
55
56/// `user_for_git_credentials`: the account password or an access token.
57#[derive(Debug, Serialize, Deserialize)]
58pub struct GitCredentialsArgs {
59 pub username: String,
60 pub secret: String,
61}
62
63/// `user_for_access_token`.
64#[derive(Debug, Serialize, Deserialize)]
65pub struct TokenArgs {
66 pub token: String,
67}
68
69/// `user_for_ssh_key`.
70#[derive(Debug, Serialize, Deserialize)]
71pub struct FingerprintArgs {
72 pub fingerprint: String,
73}
74
75/// `user_by_username`.
76#[derive(Debug, Serialize, Deserialize)]
77pub struct UsernameArgs {
78 pub username: String,
79}
80
81/// `usernames`: the names behind account and workspace ids, as events and
82/// other records store them. Returns a map from id to name; ids it does
83/// not know are left out.
84#[derive(Debug, Serialize, Deserialize)]
85pub struct UsernamesArgs {
86 pub ids: Vec<String>,
87}
88
89/// `list_ssh_keys` and `list_access_tokens`.
90#[derive(Debug, Serialize, Deserialize)]
91pub struct UserArgs {
92 pub user: User,
93}
94
95/// `add_ssh_key`: `public_key` is one line in OpenSSH format.
96/// Returns `Outcome<SshKey>`.
97#[derive(Debug, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct AddSshKeyArgs {
100 pub user: User,
101 pub title: String,
102 pub public_key: String,
103}
104
105/// `remove_ssh_key` and `remove_access_token`.
106#[derive(Debug, Serialize, Deserialize)]
107pub struct RemoveArgs {
108 pub user: User,
109 pub id: String,
110}
111
112/// `create_access_token`: a token that acts as `user`. For a workspace
113/// acting through a token of its own, the new token belongs to that
114/// workspace too.
115#[derive(Debug, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct CreateAccessTokenArgs {
118 pub user: User,
119 pub name: String,
120 /// When set, the token stops working after this many seconds and is
121 /// left out of the user's token list. Used for hosted attempts.
122 #[serde(default)]
123 pub ttl_seconds: Option<u64>,
124}
125
126/// The plaintext token is returned once and never stored.
127#[derive(Debug, Serialize, Deserialize)]
128pub struct CreatedAccessToken {
129 pub token: String,
130 pub info: AccessToken,
131}
132
133/// `register`: creates an account and signs it in.
134/// Returns `Outcome<SignedIn>`.
135#[derive(Debug, Serialize, Deserialize)]
136pub struct RegisterArgs {
137 pub username: String,
138 pub email: String,
139 pub password: String,
140}
141
142/// `verify_email`: the token from the emailed link. Returns `Outcome<User>`.
143#[derive(Debug, Serialize, Deserialize)]
144pub struct EmailTokenArgs {
145 pub token: String,
146}
147
148/// `request_password_reset`. Always succeeds, so it cannot be used to find
149/// out which addresses have accounts.
150#[derive(Debug, Serialize, Deserialize)]
151pub struct EmailArgs {
152 pub email: String,
153}
154
155/// `reset_password`: sets a new password and ends every session.
156/// Returns `Outcome<User>`.
157#[derive(Debug, Serialize, Deserialize)]
158pub struct ResetPasswordArgs {
159 pub token: String,
160 pub password: String,
161}
162
163/// `device_start`: begins a device sign-in. Returns `DeviceStart`.
164#[derive(Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct DeviceStartArgs {
167 /// What is asking, shown to the person approving, e.g. "Claude Code".
168 pub client_name: String,
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DeviceStart {
174 /// Secret held by the tool and exchanged for a token once approved.
175 pub device_code: String,
176 /// Short code shown to the person, e.g. `WDJB-MJHT`.
177 pub user_code: String,
178 /// Seconds until both codes stop working.
179 pub expires_in: u32,
180 /// Seconds the tool should wait between polls.
181 pub interval: u32,
182}
183
184/// `device_lookup`: what a user code is asking for, or null if it is not
185/// valid. Returns `Option<DeviceRequest>`.
186#[derive(Debug, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct DeviceLookupArgs {
189 pub user_code: String,
190}
191
192#[derive(Debug, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct DeviceRequest {
195 pub user_code: String,
196 pub client_name: String,
197}
198
199/// `device_resolve`: the signed-in person approves or denies a request.
200/// Returns `Outcome<bool>`.
201#[derive(Debug, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct DeviceResolveArgs {
204 pub user_code: String,
205 pub user: User,
206 pub approve: bool,
207}
208
209/// `device_claim`: the tool asks whether its request was approved.
210#[derive(Debug, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct DeviceClaimArgs {
213 pub device_code: String,
214}
215
216/// The answer to a `device_claim`.
217#[derive(Debug, Serialize, Deserialize)]
218#[serde(tag = "status", rename_all = "snake_case")]
219pub enum DeviceClaim {
220 /// Nobody has approved or denied it yet; ask again after the interval.
221 Pending,
222 Denied,
223 /// The code was never issued, has expired, or was already used.
224 Expired,
225 /// The access token, returned once.
226 Approved {
227 token: String,
228 user: User,
229 },
230}
231
232/// A workspace: the owner of repositories, and the first segment of their
233/// URLs. A person's own space and a team's are the same thing.
234#[derive(Clone, Debug, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Workspace {
237 pub id: String,
238 pub slug: String,
239 pub name: String,
240 /// One line saying what the workspace is for.
241 pub description: Option<String>,
242 /// RFC 3339.
243 pub created_at: String,
244 pub member_count: u32,
245 /// The workspace's uploaded icon: the SHA-256 of its bytes, served at
246 /// `/avatars/<avatar>`. Null means the generated letter avatar.
247 #[serde(default)]
248 pub avatar: Option<String>,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct Member {
253 pub username: String,
254 pub role: crate::Role,
255}
256
257/// `create_workspace`. Returns `Outcome<Workspace>`.
258#[derive(Debug, Serialize, Deserialize)]
259pub struct CreateWorkspaceArgs {
260 pub user: User,
261 pub slug: String,
262 #[serde(default)]
263 pub name: String,
264}
265
266/// `get_workspace`: public details, or null. Returns `Option<Workspace>`.
267#[derive(Debug, Serialize, Deserialize)]
268pub struct SlugArgs {
269 pub slug: String,
270}
271
272/// `list_members`: members only. Returns `Outcome<Vec<Member>>`.
273#[derive(Debug, Serialize, Deserialize)]
274pub struct ListMembersArgs {
275 pub slug: String,
276 pub viewer: crate::Viewer,
277}
278
279/// `add_member` and `remove_member`: owners only.
280/// Each returns `Outcome<bool>`.
281#[derive(Debug, Serialize, Deserialize)]
282pub struct MemberArgs {
283 pub actor: User,
284 pub slug: String,
285 pub username: String,
286}
287
288/// `update_workspace`: owners only. An empty name falls back to the slug;
289/// an empty description clears it. Returns `Outcome<Workspace>`.
290#[derive(Debug, Serialize, Deserialize)]
291pub struct UpdateWorkspaceArgs {
292 pub actor: User,
293 pub slug: String,
294 pub name: String,
295 pub description: String,
296}
297
298/// `rename_workspace`: owners only. Changes the workspace's slug, the first
299/// segment of its URLs, to `new_slug`; the display name is untouched. The
300/// old slug redirects to the new one, and is held for this workspace, for
301/// [`SLUG_HOLD_DAYS`]. Publishes `workspace.renamed`. Returns
302/// `Outcome<Workspace>`.
303///
304/// `check_workspace_rename` takes the same arguments and answers whether
305/// the rename would be allowed, changing nothing. Returns `Outcome<bool>`.
306#[derive(Debug, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct RenameWorkspaceArgs {
309 pub actor: User,
310 pub slug: String,
311 pub new_slug: String,
312}
313
314/// How long a workspace's old slug keeps redirecting to it, and stays
315/// reserved for it, after a rename.
316pub const SLUG_HOLD_DAYS: u64 = 90;
317
318/// How long a workspace must wait between renames.
319pub const RENAME_COOLDOWN_HOURS: u64 = 24;
320
321// `resolve_slug` takes `SlugArgs` and returns `Option<String>`: the
322// workspace's current slug when `slug` is one it was renamed from within
323// the last `SLUG_HOLD_DAYS`, and null otherwise (including for a slug that
324// is in use).
325
326/// `set_workspace_avatar`: owners only. `image` is the file's bytes in
327/// base64: PNG, JPEG, WebP or GIF, at most `MAX_AVATAR_BYTES`. Null removes
328/// the icon. Returns `Outcome<Workspace>`.
329#[derive(Debug, Serialize, Deserialize)]
330pub struct SetWorkspaceAvatarArgs {
331 pub actor: User,
332 pub slug: String,
333 pub image: Option<String>,
334}
335
336/// `set_user_avatar`: a person's own avatar, as `SetWorkspaceAvatarArgs`.
337/// Returns `Outcome<Option<String>>`: the new avatar, or null once removed.
338#[derive(Debug, Serialize, Deserialize)]
339pub struct SetUserAvatarArgs {
340 pub user: User,
341 pub image: Option<String>,
342}
343
344/// The largest avatar that can be uploaded, in bytes.
345pub const MAX_AVATAR_BYTES: usize = 1024 * 1024;
346
347/// `list_workspace_tokens`: members only. Returns
348/// `Outcome<Vec<AccessToken>>`.
349#[derive(Debug, Serialize, Deserialize)]
350pub struct WorkspaceTokensArgs {
351 pub slug: String,
352 pub viewer: crate::Viewer,
353}
354
355/// `create_workspace_token`: owners only. The token belongs to the
356/// workspace, acts as it, and keeps working when the member who made it
357/// leaves. Returns `Outcome<CreatedAccessToken>`.
358#[derive(Debug, Serialize, Deserialize)]
359pub struct CreateWorkspaceTokenArgs {
360 pub actor: User,
361 pub slug: String,
362 pub name: String,
363}
364
365/// `remove_workspace_token`: owners only. Returns `Outcome<bool>`.
366#[derive(Debug, Serialize, Deserialize)]
367pub struct RemoveWorkspaceTokenArgs {
368 pub actor: User,
369 pub slug: String,
370 pub id: String,
371}
372
373/// `oauth_authorize`: the signed-in person approved an application. The
374/// caller has checked the client and that it may be redirected to
375/// `redirect_uri`. Returns `OAuthCode`.
376#[derive(Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct OAuthAuthorizeArgs {
379 pub user: User,
380 pub client_id: String,
381 /// Shown wherever the application's access is listed.
382 pub client_name: String,
383 pub redirect_uri: String,
384 /// PKCE challenge, method S256.
385 pub code_challenge: String,
386}
387
388#[derive(Debug, Serialize, Deserialize)]
389pub struct OAuthCode {
390 pub code: String,
391}
392
393/// `oauth_exchange`: redeems an authorization code.
394/// Returns `Outcome<OAuthTokens>`.
395#[derive(Debug, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct OAuthExchangeArgs {
398 pub code: String,
399 pub code_verifier: String,
400 pub client_id: String,
401 pub redirect_uri: String,
402}
403
404/// `oauth_refresh`: trades a refresh token for new tokens.
405/// Returns `Outcome<OAuthTokens>`.
406#[derive(Debug, Serialize, Deserialize)]
407#[serde(rename_all = "camelCase")]
408pub struct OAuthRefreshArgs {
409 pub refresh_token: String,
410 pub client_id: String,
411}
412
413#[derive(Debug, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct OAuthTokens {
416 pub access_token: String,
417 /// Works once; using it returns the next one.
418 pub refresh_token: String,
419 /// Seconds until the access token stops working.
420 pub expires_in: u64,
421}
422
423/// An application a person has signed in to. Listed by `list_oauth_grants`
424/// and ended by `revoke_oauth_grant`.
425#[derive(Debug, Serialize, Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub struct OAuthGrant {
428 pub id: String,
429 pub client_name: String,
430 /// RFC 3339.
431 pub created_at: String,
432 /// RFC 3339.
433 pub last_used_at: String,
434}
435
436
437/// What an agent's token may do: these operations, in this repository.
438#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
439pub struct AgentScope {
440 pub repo: crate::repos::RepoPath,
441 /// API and MCP operation names, such as `create_issue`.
442 pub operations: Vec<String>,
443 /// Set on a run credential: the run it belongs to, and what it may do
444 /// with git. See [`crate::credentials`].
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub run: Option<crate::credentials::RunBinding>,
447}
448
449/// `create_agent_token`: a token for a g1t agent working on someone's
450/// behalf. It acts as `g1t-agent`, a member of the repository's workspace,
451/// and only for the operations in `scope`. Returns `CreatedAccessToken`.
452#[derive(Debug, Serialize, Deserialize)]
453#[serde(rename_all = "camelCase")]
454pub struct CreateAgentTokenArgs {
455 /// The person the agent works for; the token is recorded as theirs.
456 pub on_behalf_of: User,
457 pub scope: AgentScope,
458 pub ttl_seconds: u64,
459}
460
461// `agent_scope` takes `TokenArgs` and returns `Option<AgentScope>`: what an
462// agent's token may do, or null for any other token.
463
464/// The id and name g1t's agents act under.
465pub const AGENT_ID: &str = "usr_g1t_agent";
466pub const AGENT_NAME: &str = "g1t-agent";
467
468// --- Staff ---------------------------------------------------------------
469//
470// Staff-only methods, for sudo.g1t.sh. They take no viewer and check no
471// membership: only sudo calls them, over its service binding, after it has
472// verified a Cloudflare Access sign-in and its staff list. Nothing a
473// customer can reach should ever forward to them.
474
475/// `notify_owners`: emails a short notice, with one link, to each owner of
476/// a workspace with a confirmed address. Called by other services (billing
477/// warns owners near their usage limit), never on a person's behalf.
478/// Returns how many were sent.
479#[derive(Clone, Debug, Serialize, Deserialize)]
480pub struct NotifyOwnersArgs {
481 pub workspace: String,
482 pub subject: String,
483 /// One or two sentences: what happened and what it means.
484 pub intro: String,
485 /// The button's words, such as `Open billing`.
486 pub action: String,
487 /// Where the button goes; must be on g1t.sh.
488 pub link: String,
489 /// Small print: why they got it.
490 pub footer: String,
491}
492
493/// `admin_workspaces`: every workspace, newest first, at most
494/// [`ADMIN_WORKSPACES_LIMIT`], optionally only those whose slug, name or
495/// an owner's username or email contains `query`. Returns
496/// `Vec<AdminWorkspace>`. Staff only.
497#[derive(Debug, Default, Serialize, Deserialize)]
498pub struct AdminWorkspacesArgs {
499 #[serde(default)]
500 pub query: Option<String>,
501}
502
503/// The most workspaces one `admin_workspaces` call returns.
504pub const ADMIN_WORKSPACES_LIMIT: usize = 500;
505
506/// An owner of a workspace, as staff see them.
507#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
508pub struct AdminOwner {
509 pub username: String,
510 pub email: Option<String>,
511}
512
513/// A workspace as staff see it: who owns it and how many belong to it.
514#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct AdminWorkspace {
517 pub slug: String,
518 pub name: String,
519 /// RFC 3339.
520 pub created_at: String,
521 pub owners: Vec<AdminOwner>,
522 pub member_count: u32,
523}
524
525/// `admin_workspace`: one workspace with every member, or null. Takes
526/// `SlugArgs`; returns `Option<AdminWorkspaceDetail>`. Staff only.
527#[derive(Clone, Debug, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub struct AdminWorkspaceDetail {
530 pub slug: String,
531 pub name: String,
532 pub description: Option<String>,
533 /// RFC 3339.
534 pub created_at: String,
535 /// Owners first, then by username.
536 pub members: Vec<AdminMember>,
537}
538
539/// A member of a workspace, as staff see them.
540#[derive(Clone, Debug, Serialize, Deserialize)]
541pub struct AdminMember {
542 pub username: String,
543 pub email: Option<String>,
544 pub role: crate::Role,
545 /// When they joined the workspace. RFC 3339.
546 pub joined: String,
547}
548
549// --- Profiles ------------------------------------------------------------
550//
551// A person's public page at `g1t.sh/u/<username>`. Everything in a
552// `Profile` is shown to anyone, signed in or not; an email address never is.
553
554/// The most characters each profile field takes.
555pub const MAX_PROFILE_NAME: usize = 80;
556pub const MAX_PROFILE_BIO: usize = 160;
557pub const MAX_PROFILE_LOCATION: usize = 80;
558pub const MAX_PROFILE_WEBSITE: usize = 200;
559pub const MAX_PROFILE_PRONOUNS: usize = 40;
560
561/// What anyone may see about a person.
562#[derive(Clone, Debug, Default, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564pub struct Profile {
565 pub username: String,
566 /// The name they go by, if they gave one.
567 pub name: Option<String>,
568 /// One or two lines about them, at most [`MAX_PROFILE_BIO`] characters.
569 pub bio: Option<String>,
570 pub location: Option<String>,
571 /// An `https://` address.
572 pub website: Option<String>,
573 pub pronouns: Option<String>,
574 /// The uploaded avatar's hash, served at `/avatars/<avatar>`.
575 pub avatar: Option<String>,
576 /// When the account was made. RFC 3339.
577 pub created_at: String,
578}
579
580// `profile` takes `UsernameArgs` and returns `Option<Profile>`: null for
581// an account that does not exist.
582
583/// `update_profile`: a person changes their own profile. Every field is
584/// replaced; an empty one is cleared. Returns `Outcome<Profile>`.
585#[derive(Debug, Default, Serialize, Deserialize)]
586#[serde(rename_all = "camelCase")]
587pub struct UpdateProfileArgs {
588 pub actor: User,
589 #[serde(default)]
590 pub name: String,
591 #[serde(default)]
592 pub bio: String,
593 #[serde(default)]
594 pub location: String,
595 #[serde(default)]
596 pub website: String,
597 #[serde(default)]
598 pub pronouns: String,
599}
600
601/// `profile_workspaces`: the workspaces shown on a person's profile, as
602/// `viewer` may see them. A membership is shown only when it is no secret
603/// from the viewer: a workspace the viewer belongs to as well, or one of
604/// `public`, the workspaces the caller found the person has made a public
605/// project in (whose page shows that already). Returns
606/// `Vec<ProfileWorkspace>`; empty for an account that does not exist.
607#[derive(Debug, Serialize, Deserialize)]
608pub struct ProfileWorkspacesArgs {
609 pub username: String,
610 pub viewer: crate::Viewer,
611 #[serde(default)]
612 pub public: Vec<String>,
613}
614
615/// A workspace on a person's profile.
616#[derive(Clone, Debug, Serialize, Deserialize)]
617pub struct ProfileWorkspace {
618 pub slug: String,
619 pub name: String,
620 pub avatar: Option<String>,
621}
622
623/// `directory`: every account or every workspace, as their public pages
624/// show them, a page at a time in name order. For services that index
625/// them, such as search; nothing private is in it. Returns
626/// `DirectoryPage`.
627#[derive(Debug, Default, Serialize, Deserialize)]
628pub struct DirectoryArgs {
629 /// `user` or `workspace`.
630 pub kind: String,
631 /// Names after this one.
632 #[serde(default)]
633 pub after: Option<String>,
634 pub limit: u32,
635}
636
637/// One account or workspace in the directory.
638#[derive(Clone, Debug, Serialize, Deserialize)]
639#[serde(rename_all = "camelCase")]
640pub struct DirectoryEntry {
641 /// The account's or workspace's id.
642 pub id: String,
643 /// A username or a workspace's slug.
644 pub slug: String,
645 /// A person's display name or a workspace's name.
646 pub name: Option<String>,
647 /// A person's bio or a workspace's description.
648 pub bio: Option<String>,
649 pub avatar: Option<String>,
650 /// RFC 3339.
651 pub created_at: String,
652}
653
654#[derive(Clone, Debug, Default, Serialize, Deserialize)]
655pub struct DirectoryPage {
656 pub entries: Vec<DirectoryEntry>,
657 /// Where the next page starts; null on the last.
658 pub next: Option<String>,
659}