Skip to content
1,964 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.

Deploy scripts live in the repository1//! 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,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca18 /// When it was last used to sign in over SSH, RFC 3339, to within 5
19 /// minutes; null when it never was.
20 #[serde(default)]
21 pub last_used_at: Option<String>,
Deploy scripts live in the repository22}
23
Fine-grained personal tokens, workspace token rules and approvals in identity24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
Deploy scripts live in the repository25#[serde(rename_all = "camelCase")]
26pub struct AccessToken {
27 pub id: String,
28 pub name: String,
29 /// RFC 3339.
30 pub created_at: String,
31 /// RFC 3339, to within a few minutes. Null until it is first used.
32 pub last_used_at: Option<String>,
33 /// For a workspace's token, the username of the member who made it.
34 /// Null once that account is gone, and on personal tokens.
35 pub created_by: Option<String>,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers36 /// Its scopes, as `resource:level`, the highest of each resource.
37 /// Null: full access (an application's or an agent's credential).
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step38 #[serde(default)]
39 pub scopes: Option<Vec<String>>,
40 /// Made before tokens had scopes: full access until someone narrows it.
41 #[serde(default)]
42 pub legacy: bool,
43 /// RFC 3339. Null: it does not expire.
44 #[serde(default)]
45 pub expires_at: Option<String>,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers46 /// Its scopes as permissions: each resource it may use, by name, at
47 /// the highest level, such as `{"issues": "write"}`. Every resource at
48 /// its highest when `scopes` is null.
Fine-grained personal tokens, workspace token rules and approvals in identity49 #[serde(default)]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers50 pub permissions: std::collections::BTreeMap<String, String>,
Fine-grained personal tokens, workspace token rules and approvals in identity51 /// What it is for, as its owner wrote it.
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub description: Option<String>,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers54 /// A personal token's reach: the workspace it is made for, by slug;
55 /// null for every workspace its owner belongs to (or, with
56 /// `repository_selection` public, none). Null on a workspace's own
57 /// token, which reaches its workspace.
58 #[serde(default)]
59 pub workspace: Option<String>,
60 /// Which repositories of that workspace it reaches.
61 #[serde(default)]
62 pub repository_selection: crate::scopes::RepositorySelection,
63 /// With `selected`: the repositories, as `owner/name`, that the viewer
64 /// can see.
65 #[serde(default)]
66 pub repositories: Vec<String>,
67 /// Whether a token made for a workspace that approves tokens may be
68 /// used there yet.
69 #[serde(default)]
70 pub status: crate::tokens::TokenStatus,
71 /// Why an owner denied or revoked it.
Fine-grained personal tokens, workspace token rules and approvals in identity72 #[serde(default, skip_serializing_if = "Option::is_none")]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers73 pub review_reason: Option<String>,
74 /// Whether it is a workspace's own token, acting as the workspace.
75 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
76 pub workspace_owned: bool,
77 /// A workspace's own token with Repositories: admin, which acts as an
78 /// admin of the workspace's repositories rather than with Write.
Fine-grained personal tokens, workspace token rules and approvals in identity79 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
80 pub admin: bool,
Merge main into Artifacts Phase 281 /// A person's token its owner let use the website (g1t.sh) as them,
82 /// with `Authorization: Bearer`. See [`crate::scopes::TokenAccess::website`].
83 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
84 pub website: bool,
Deploy scripts live in the repository85}
86
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87/// `sign_in`: verifies a username, or any confirmed email address of the
88/// account, and its password, for website sign-in. Wrong passwords are
89/// counted against the account and `client`, and past a limit nothing is
90/// checked for a while (see identity's `throttle.rs`).
Deploy scripts live in the repository91/// Returns `Outcome<SignedIn>`.
92#[derive(Debug, Serialize, Deserialize)]
93pub struct SignInArgs {
94 pub username: String,
95 pub password: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look96 /// Who is asking, such as the visitor's IP address, for rate limits.
97 #[serde(default)]
98 pub client: Option<String>,
Deploy scripts live in the repository99}
100
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct SignedIn {
104 pub user: User,
Merge main (membership, two-factor, GitHub repo roles) into tokens105 /// Empty while `two_factor_challenge` is set: no session is made until
106 /// the code is given.
Deploy scripts live in the repository107 pub session_token: String,
Merge main (membership, two-factor, GitHub repo roles) into tokens108 /// Set when the account has two-factor authentication on: the token to
109 /// pass to `two_factor_sign_in` with a code. Valid for
110 /// `accounts::TWO_FACTOR_CHALLENGE_SECONDS`.
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub two_factor_challenge: Option<String>,
Deploy scripts live in the repository113}
114
115/// `sign_out` and `user_for_session`.
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct SessionArgs {
119 pub session_token: String,
120}
121
122/// `user_for_git_credentials`: the account password or an access token.
123#[derive(Debug, Serialize, Deserialize)]
124pub struct GitCredentialsArgs {
125 pub username: String,
126 pub secret: String,
127}
128
129/// `user_for_access_token`.
130#[derive(Debug, Serialize, Deserialize)]
131pub struct TokenArgs {
132 pub token: String,
133}
134
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca135/// `user_for_ssh_key`, and `principal_for_ssh_key` (see [`crate::deploy_keys`]).
Deploy scripts live in the repository136#[derive(Debug, Serialize, Deserialize)]
137pub struct FingerprintArgs {
138 pub fingerprint: String,
139}
140
141/// `user_by_username`.
142#[derive(Debug, Serialize, Deserialize)]
143pub struct UsernameArgs {
144 pub username: String,
145}
146
147/// `usernames`: the names behind account and workspace ids, as events and
148/// other records store them. Returns a map from id to name; ids it does
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97149/// not know are left out. Also `accounts`: the accounts behind user ids,
150/// each with its username and avatar (`HashMap<String, accounts::EmailOwner>`).
Deploy scripts live in the repository151#[derive(Debug, Serialize, Deserialize)]
152pub struct UsernamesArgs {
153 pub ids: Vec<String>,
154}
155
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar156/// `display_usernames`: how each of these people (by lowercased username,
157/// at most 200) wrote their username, for showing it beside the key.
158/// Returns a map from the lowercased username to its chosen case; people
159/// who chose none, and names nobody has, are left out.
160#[derive(Debug, Default, Serialize, Deserialize)]
161pub struct DisplayUsernamesArgs {
162 pub usernames: Vec<String>,
163}
164
Deploy scripts live in the repository165/// `list_ssh_keys` and `list_access_tokens`.
166#[derive(Debug, Serialize, Deserialize)]
167pub struct UserArgs {
168 pub user: User,
169}
170
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge171/// `ssh_key_owners`: services only. The account (user id) that registered
172/// each key, by fingerprint (`SHA256:…`, as `ssh-keygen -lf` prints it),
173/// for verifying commits signed with SSH keys. Returns a map of the
174/// fingerprints found to user ids.
175#[derive(Debug, Serialize, Deserialize)]
176pub struct SshKeyOwnersArgs {
177 pub fingerprints: Vec<String>,
178}
179
Deploy scripts live in the repository180/// `add_ssh_key`: `public_key` is one line in OpenSSH format.
181/// Returns `Outcome<SshKey>`.
182#[derive(Debug, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct AddSshKeyArgs {
185 pub user: User,
186 pub title: String,
187 pub public_key: String,
188}
189
190/// `remove_ssh_key` and `remove_access_token`.
191#[derive(Debug, Serialize, Deserialize)]
192pub struct RemoveArgs {
193 pub user: User,
194 pub id: String,
195}
196
197/// `create_access_token`: a token that acts as `user`. For a workspace
198/// acting through a token of its own, the new token belongs to that
199/// workspace too.
200#[derive(Debug, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct CreateAccessTokenArgs {
203 pub user: User,
204 pub name: String,
205 /// When set, the token stops working after this many seconds and is
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step206 /// left out of the user's token list, unless `listed`. Used for hosted
207 /// attempts.
Deploy scripts live in the repository208 #[serde(default)]
209 pub ttl_seconds: Option<u64>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step210 /// Its scopes, as `resource:level`; unknown names are left out. Null:
211 /// full access.
212 #[serde(default)]
213 pub scopes: Option<Vec<String>>,
214 /// Listed with the person's tokens although it expires: one they made
215 /// themselves, with an expiry.
216 #[serde(default)]
217 pub listed: bool,
218}
219
Merge branch 'worktree-agent-a3abfcce648e87dca'220/// `create_job_token`: a workflow job's `G1T_TOKEN`. It acts as the
221/// repository's workspace, reaches that repository only, holds `scopes`
222/// (from the job's `permissions`), and is never listed. The actions service
223/// revokes it when the job ends (`revoke_job_tokens`); `ttl_seconds` is a
224/// backstop. Returns `CreatedAccessToken`.
225#[derive(Debug, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct CreateJobTokenArgs {
228 /// The workspace the repository belongs to, as its own principal.
229 pub workspace: User,
230 pub repo: crate::repos::RepoPath,
231 pub run_id: String,
232 pub job_id: String,
233 /// What the token is listed as in logs: `G1T_TOKEN for acme/web run 4`.
234 pub name: String,
235 pub ttl_seconds: u64,
236 /// As `resource:level`; unknown names are left out.
237 pub scopes: Vec<String>,
238 /// Whether it may open and approve pull requests (`JobToken::pull_requests`).
239 #[serde(default)]
240 pub pull_requests: bool,
241}
242
243/// `revoke_job_tokens`: ends a workflow job's tokens at once, when the job
244/// finishes or is cancelled. Only job tokens are touched. Returns `bool`.
245#[derive(Debug, Default, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct RevokeJobTokensArgs {
248 pub job_id: String,
249}
250
Deploy scripts live in the repository251/// The plaintext token is returned once and never stored.
252#[derive(Debug, Serialize, Deserialize)]
253pub struct CreatedAccessToken {
254 pub token: String,
255 pub info: AccessToken,
256}
257
258/// `register`: creates an account and signs it in.
259/// Returns `Outcome<SignedIn>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look260///
261/// While registration is invite-only (`REGISTRATION_MODE=invite`), every
262/// new account needs `invite_code`: an unused, unexpired invite, and, when
263/// the invite names an email, that address. See [`CreateInviteArgs`].
Deploy scripts live in the repository264#[derive(Debug, Serialize, Deserialize)]
265pub struct RegisterArgs {
266 pub username: String,
267 pub email: String,
268 pub password: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look269 /// An invite code such as `g1t-k7m2-q9xd-4hpw-…`. Ignored while
270 /// registration is open.
271 #[serde(default)]
272 pub invite_code: Option<String>,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm273 /// The `proof` from the invite email's link. When it is the invite's
274 /// own and `email` is the address the invite was sent to, the account
275 /// starts with that address confirmed; otherwise it is ignored.
276 #[serde(default)]
277 pub email_proof: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 /// Who is asking, such as the visitor's IP address, for rate limits.
279 #[serde(default)]
280 pub client: Option<String>,
Deploy scripts live in the repository281}
282
283/// `verify_email`: the token from the emailed link. Returns `Outcome<User>`.
284#[derive(Debug, Serialize, Deserialize)]
285pub struct EmailTokenArgs {
286 pub token: String,
287}
288
289/// `request_password_reset`. Always succeeds, so it cannot be used to find
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look290/// out which addresses have accounts. Any confirmed address of an account
291/// works: the link goes to the address given, and the primary (and the
292/// backup) are told a reset was asked for. A few requests an hour per
293/// address and per `client`; past that, nothing is sent.
Deploy scripts live in the repository294#[derive(Debug, Serialize, Deserialize)]
295pub struct EmailArgs {
296 pub email: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look297 /// Who is asking, such as the visitor's IP address, for rate limits.
298 #[serde(default)]
299 pub client: Option<String>,
Deploy scripts live in the repository300}
301
302/// `reset_password`: sets a new password and ends every session.
303/// Returns `Outcome<User>`.
304#[derive(Debug, Serialize, Deserialize)]
305pub struct ResetPasswordArgs {
306 pub token: String,
307 pub password: String,
308}
309
310/// `device_start`: begins a device sign-in. Returns `DeviceStart`.
311#[derive(Debug, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct DeviceStartArgs {
314 /// What is asking, shown to the person approving, e.g. "Claude Code".
315 pub client_name: String,
316}
317
318#[derive(Debug, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct DeviceStart {
321 /// Secret held by the tool and exchanged for a token once approved.
322 pub device_code: String,
323 /// Short code shown to the person, e.g. `WDJB-MJHT`.
324 pub user_code: String,
325 /// Seconds until both codes stop working.
326 pub expires_in: u32,
327 /// Seconds the tool should wait between polls.
328 pub interval: u32,
329}
330
331/// `device_lookup`: what a user code is asking for, or null if it is not
332/// valid. Returns `Option<DeviceRequest>`.
333#[derive(Debug, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct DeviceLookupArgs {
336 pub user_code: String,
337}
338
339#[derive(Debug, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct DeviceRequest {
342 pub user_code: String,
343 pub client_name: String,
344}
345
346/// `device_resolve`: the signed-in person approves or denies a request.
347/// Returns `Outcome<bool>`.
348#[derive(Debug, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct DeviceResolveArgs {
351 pub user_code: String,
352 pub user: User,
353 pub approve: bool,
354}
355
356/// `device_claim`: the tool asks whether its request was approved.
357#[derive(Debug, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct DeviceClaimArgs {
360 pub device_code: String,
361}
362
363/// The answer to a `device_claim`.
364#[derive(Debug, Serialize, Deserialize)]
365#[serde(tag = "status", rename_all = "snake_case")]
366pub enum DeviceClaim {
367 /// Nobody has approved or denied it yet; ask again after the interval.
368 Pending,
369 Denied,
370 /// The code was never issued, has expired, or was already used.
371 Expired,
372 /// The access token, returned once.
373 Approved {
374 token: String,
375 user: User,
376 },
377}
378
379/// A workspace: the owner of repositories, and the first segment of their
380/// URLs. A person's own space and a team's are the same thing.
381#[derive(Clone, Debug, Serialize, Deserialize)]
382#[serde(rename_all = "camelCase")]
383pub struct Workspace {
384 pub id: String,
385 pub slug: String,
386 pub name: String,
387 /// One line saying what the workspace is for.
388 pub description: Option<String>,
389 /// RFC 3339.
390 pub created_at: String,
391 pub member_count: u32,
Workspace names and icons, and a component kit for every control392 /// The workspace's uploaded icon: the SHA-256 of its bytes, served at
393 /// `/avatars/<avatar>`. Null means the generated letter avatar.
394 #[serde(default)]
395 pub avatar: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look396 /// What every member gets on each of its repositories; owners have
397 /// Admin. See [`crate::access`].
398 #[serde(default)]
399 pub base_permission: crate::access::BasePermission,
Merge branch 'worktree-agent-ad7c6d88d93adc817'400 /// Who may create its teams. See [`crate::teams::TeamCreation`].
401 #[serde(default)]
402 pub team_creation: crate::teams::TeamCreation,
Merge main (membership, two-factor, GitHub repo roles) into tokens403 /// What members may do, by GitHub's names for each
404 /// (`members_can_create_public_repositories`...), at the top level as
405 /// GitHub's organization has them. See [`crate::MemberPrivileges`].
406 #[serde(flatten)]
407 pub privileges: crate::MemberPrivileges,
408 /// Whether members and outside collaborators need two-factor
409 /// authentication to use it.
410 #[serde(default)]
411 pub two_factor_requirement_enabled: bool,
Deploy scripts live in the repository412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct Member {
416 pub username: String,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar417 /// The username as its owner wrote it (`Ana`), when that differs from
418 /// `username`: what pages show.
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub display_username: Option<String>,
Deploy scripts live in the repository421 pub role: crate::Role,
Merge main (membership, two-factor, GitHub repo roles) into tokens422 /// The roles they hold besides `role`.
423 #[serde(default)]
424 pub org_roles: Vec<crate::OrgRole>,
425 /// Whether they have two-factor authentication on. Shown to owners
426 /// only; null for anyone else.
427 #[serde(default)]
428 pub two_factor: Option<bool>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look429 /// Their display name, when they set one.
430 #[serde(default)]
431 pub name: Option<String>,
432 /// Their uploaded avatar: the SHA-256 of its bytes, served at
433 /// `/avatars/<avatar>`. None means the generated letter avatar.
434 #[serde(default)]
435 pub avatar: Option<String>,
Deploy scripts live in the repository436}
437
Merge branch 'worktree-agent-a2013627e5ea4ab13'438/// Where a workspace keeps its repositories' git data: anywhere g1t
439/// stores it (the default), or in the EU only. It applies to repositories
440/// made after it is set; the repos service reads it when it places a new
441/// one (`storage_options` says whether the EU can be chosen).
442#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(rename_all = "lowercase")]
444pub enum DataResidency {
445 #[default]
446 Anywhere,
447 Eu,
448}
449
450impl DataResidency {
451 pub fn as_str(self) -> &'static str {
452 match self {
453 DataResidency::Anywhere => "anywhere",
454 DataResidency::Eu => "eu",
455 }
456 }
457
458 pub fn parse(text: &str) -> Option<Self> {
459 match text.trim().to_ascii_lowercase().as_str() {
460 "anywhere" => Some(DataResidency::Anywhere),
461 "eu" => Some(DataResidency::Eu),
462 _ => None,
463 }
464 }
465}
466
467/// `workspace_residency` takes [`SlugArgs`] and returns
468/// `Option<DataResidency>` (null when there is no such workspace).
469/// `set_workspace_residency`: owners only. Returns `Outcome<DataResidency>`.
470#[derive(Debug, Serialize, Deserialize)]
471pub struct SetResidencyArgs {
472 pub actor: User,
473 pub slug: String,
474 pub residency: DataResidency,
475}
476
Deploy scripts live in the repository477/// `create_workspace`. Returns `Outcome<Workspace>`.
478#[derive(Debug, Serialize, Deserialize)]
479pub struct CreateWorkspaceArgs {
480 pub user: User,
481 pub slug: String,
482 #[serde(default)]
483 pub name: String,
484}
485
486/// `get_workspace`: public details, or null. Returns `Option<Workspace>`.
487#[derive(Debug, Serialize, Deserialize)]
488pub struct SlugArgs {
489 pub slug: String,
490}
491
Merge main (membership, two-factor, GitHub repo roles) into tokens492/// `list_members`: members only. Owners also see each member's
493/// `two_factor`. Returns `Outcome<Vec<Member>>`.
Deploy scripts live in the repository494#[derive(Debug, Serialize, Deserialize)]
495pub struct ListMembersArgs {
496 pub slug: String,
497 pub viewer: crate::Viewer,
498}
499
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)500/// `add_member` and `remove_member`: owners only. `add_member` never adds a
501/// person at once: it sends them a workspace invitation to accept or
502/// decline, as `invite_member` with a username does. Only g1t's own agent
503/// is added at once. Removing yourself is
Merge main (membership, two-factor, GitHub repo roles) into tokens504/// leaving (`members::LeaveWorkspaceArgs`); removing an owner is refused
505/// when they are the last. Each returns `Outcome<bool>`.
Deploy scripts live in the repository506#[derive(Debug, Serialize, Deserialize)]
507pub struct MemberArgs {
508 pub actor: User,
509 pub slug: String,
510 pub username: String,
Merge main (membership, two-factor, GitHub repo roles) into tokens511 #[serde(default)]
512 pub surface: Option<crate::audit::Surface>,
Deploy scripts live in the repository513}
514
515/// `update_workspace`: owners only. An empty name falls back to the slug;
516/// an empty description clears it. Returns `Outcome<Workspace>`.
517#[derive(Debug, Serialize, Deserialize)]
518pub struct UpdateWorkspaceArgs {
519 pub actor: User,
520 pub slug: String,
521 pub name: String,
522 pub description: String,
523}
524
Agents and memory, checks and conflicts, profiles, slug renames, custom domains525/// `rename_workspace`: owners only. Changes the workspace's slug, the first
526/// segment of its URLs, to `new_slug`; the display name is untouched. The
527/// old slug redirects to the new one, and is held for this workspace, for
528/// [`SLUG_HOLD_DAYS`]. Publishes `workspace.renamed`. Returns
529/// `Outcome<Workspace>`.
530///
531/// `check_workspace_rename` takes the same arguments and answers whether
532/// the rename would be allowed, changing nothing. Returns `Outcome<bool>`.
533#[derive(Debug, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct RenameWorkspaceArgs {
536 pub actor: User,
537 pub slug: String,
538 pub new_slug: String,
539}
540
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look541/// `delete_workspace`: owners only, and only a person. `confirm` must be
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member542/// the workspace's slug, typed out. Refused for a protected workspace
543/// ([`protected_names`]), whoever asks, and while billing cannot settle it
544/// (`close_workspace`). Everything in it goes with it at once: nobody can
545/// reach it, its tokens stop working, its pages are not found, and its
546/// repositories, projects and apps are deleted with it. It is kept for
547/// [`WORKSPACE_RESTORE_DAYS`] so g1t's staff can restore it, then purged:
548/// its memberships, access tokens and old-slug redirects go, and billing's
549/// ledger and the audit log keep its history. The slug is never given to
550/// another workspace; the person whose username it is may make a workspace
551/// of that name again once it is purged. Publishes `workspace.deleting`,
552/// and `workspace.deleted` at the purge. Returns `Outcome<bool>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look553///
554/// `check_workspace_deletion` takes the same arguments (with `confirm`
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member555/// ignored) and says what would go and whether anything stands in the way,
556/// changing nothing. Returns `Outcome<WorkspaceDeletion>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look557#[derive(Debug, Serialize, Deserialize)]
558pub struct DeleteWorkspaceArgs {
559 pub actor: User,
560 pub slug: String,
561 #[serde(default)]
562 pub confirm: String,
563 /// Where the request came in, for the audit log; g1t.sh when absent.
564 #[serde(default)]
565 pub surface: Option<crate::audit::Surface>,
566}
567
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member568/// What deleting a workspace takes with it, and what stands in the way.
569/// Nothing does when `billing` is null and it is not `protected`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look570#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
571pub struct WorkspaceDeletion {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member572 /// Its live repositories, which are deleted with it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look573 pub repositories: u32,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member574 /// Its projects, hidden with it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look575 pub projects: u32,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member576 #[serde(default)]
577 pub members: u32,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look578 /// Why billing cannot close the workspace yet, in words for its owner.
579 pub billing: Option<String>,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member580 /// It can never be deleted, by anyone ([`protected_names`]).
581 #[serde(default)]
582 pub protected: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583}
584
585impl WorkspaceDeletion {
586 pub fn blocked(&self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member587 self.protected || self.billing.is_some()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look588 }
589
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member590 /// Why the workspace cannot be deleted, as one sentence, or `None`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look591 pub fn reason(&self, slug: &str) -> Option<String> {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member592 if self.protected {
593 return Some(protected_refusal(slug));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look594 }
595 self.billing.clone()
596 }
597}
598
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member599/// How long a deleted workspace is kept, for staff to restore, before it is
600/// purged.
601pub const WORKSPACE_RESTORE_DAYS: u64 = 30;
602
603/// Workspaces nobody can delete, whatever identity's `PROTECTED_WORKSPACES`
604/// says: Flagon's, which runs g1t.
605pub const ALWAYS_PROTECTED: &[&str] = &["flagon-io"];
606
607/// The protected workspaces: `configured` (comma-separated slugs or
608/// workspace ids, as identity's `PROTECTED_WORKSPACES` holds them), and
609/// [`ALWAYS_PROTECTED`] whatever it says, so an empty or missing variable
610/// still protects them. Lowercased, without duplicates.
611pub fn protected_names(configured: Option<&str>) -> Vec<String> {
612 let mut names: Vec<String> = Vec::new();
613 let given = configured.unwrap_or_default().split(',');
614 for name in ALWAYS_PROTECTED.iter().copied().chain(given) {
615 let name = name.trim().to_lowercase();
616 if !name.is_empty() && !names.contains(&name) {
617 names.push(name);
618 }
619 }
620 names
621}
622
623/// Why a protected workspace is not deleted, purged or acted on.
624pub fn protected_refusal(slug: &str) -> String {
625 format!("{slug} is protected and can never be deleted.")
626}
627
628/// `admin_deleted_workspaces` takes no arguments (`{}`) and returns
629/// `Vec<DeletedWorkspace>`, newest first. Staff only.
630///
631/// A workspace an owner deleted, kept until `purge_after` for staff to
632/// restore.
633#[derive(Clone, Debug, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct DeletedWorkspace {
636 pub workspace_id: String,
637 pub slug: String,
638 pub name: String,
639 /// RFC 3339.
640 pub deleted_at: String,
Merge sudo: delete an account with the workspaces it alone owns, purge each641 /// The username of the owner who deleted it, or the staff member (by
642 /// email) who deleted it with the account that was its only owner.
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member643 pub deleted_by: String,
644 /// RFC 3339: when it is purged unless restored first.
645 pub purge_after: String,
646 /// What went with it, counted when it was deleted.
647 pub went: WorkspaceDeletion,
648 /// Whether staff can still restore it.
649 pub restorable: bool,
650}
651
652/// `admin_restore_workspace` and `admin_purge_workspace`: staff restore a
653/// deleted workspace within [`WORKSPACE_RESTORE_DAYS`], or purge it now.
654/// `staff` is who, for the audit logs. Purging needs `confirm`, the slug
655/// typed out, and is refused for a protected workspace. Restoring publishes
656/// `workspace.restored`; purging, `workspace.deleted`. Both return
657/// `Outcome<bool>`.
658#[derive(Debug, Serialize, Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct AdminDeletedWorkspaceArgs {
661 pub workspace_id: String,
662 pub staff: String,
663 #[serde(default)]
664 pub confirm: String,
665}
666
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look667/// `transfer_repo_scopes`: a repository moved from `from` to `to`; the
668/// tokens of agents at work on it are kept pointing at it. For repos'
669/// `transfer`. Returns `bool`.
670#[derive(Debug, Serialize, Deserialize)]
671pub struct TransferRepoScopesArgs {
672 pub from: crate::repos::RepoPath,
673 pub to: crate::repos::RepoPath,
674}
675
Agents and memory, checks and conflicts, profiles, slug renames, custom domains676/// How long a workspace's old slug keeps redirecting to it, and stays
677/// reserved for it, after a rename.
678pub const SLUG_HOLD_DAYS: u64 = 90;
679
680/// How long a workspace must wait between renames.
681pub const RENAME_COOLDOWN_HOURS: u64 = 24;
682
683// `resolve_slug` takes `SlugArgs` and returns `Option<String>`: the
684// workspace's current slug when `slug` is one it was renamed from within
685// the last `SLUG_HOLD_DAYS`, and null otherwise (including for a slug that
Merge branch 'worktree-agent-a8385d293d42c913a'686// is in use), or the workspace's slug when `slug` is one of its aliases.
687
688// `resolve_alias` takes `SlugArgs` and returns `Option<String>`: the slug
689// now of the workspace `slug` is an alias of, and null when it is none.
690// Aliases are set by g1t's staff only: `g1t` is Flagon, Inc.'s `flagon-io`.
691// An alias follows its workspace through renames.
692
693/// `admin_aliases` takes no arguments (`{}`) and returns
694/// `Vec<WorkspaceAlias>`, by alias. Staff only.
695///
696/// A name staff point at a workspace, so that its addresses (pages, git,
697/// the API, packages) lead to the workspace under its own name.
698#[derive(Clone, Debug, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct WorkspaceAlias {
701 pub alias: String,
702 pub workspace_id: String,
703 /// The workspace's slug and name now.
704 pub workspace: String,
705 pub workspace_name: String,
706 /// Why it exists, as staff wrote it.
707 pub note: String,
708 /// The staff member who set it, or `migration`.
709 pub created_by: String,
710 /// RFC 3339.
711 pub created_at: String,
712}
713
714/// `admin_set_alias`: points `alias` at the workspace whose slug is
715/// `workspace`. The alias must have a namespace's shape, must not be one of
716/// the site's routes, and must not be anyone's username, a workspace's slug
717/// (deleted, or held after a rename) or another alias. `note` is required:
718/// it is the reason, kept with the alias and in sudo's audit log. Staff
719/// only. Returns `Outcome<WorkspaceAlias>`.
720#[derive(Debug, Serialize, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct AdminSetAliasArgs {
723 pub alias: String,
724 pub workspace: String,
725 pub note: String,
726 pub staff: String,
727}
728
729/// `admin_remove_alias`: the alias stops leading anywhere, and the name is
730/// nobody's again unless it is reserved. `reason` goes in sudo's audit log.
731/// Staff only. Returns `Outcome<bool>`.
732#[derive(Debug, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct AdminRemoveAliasArgs {
735 pub alias: String,
736 pub reason: String,
737 pub staff: String,
738}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains739
Workspace names and icons, and a component kit for every control740/// `set_workspace_avatar`: owners only. `image` is the file's bytes in
741/// base64: PNG, JPEG, WebP or GIF, at most `MAX_AVATAR_BYTES`. Null removes
742/// the icon. Returns `Outcome<Workspace>`.
743#[derive(Debug, Serialize, Deserialize)]
744pub struct SetWorkspaceAvatarArgs {
745 pub actor: User,
746 pub slug: String,
747 pub image: Option<String>,
748}
749
750/// `set_user_avatar`: a person's own avatar, as `SetWorkspaceAvatarArgs`.
751/// Returns `Outcome<Option<String>>`: the new avatar, or null once removed.
752#[derive(Debug, Serialize, Deserialize)]
753pub struct SetUserAvatarArgs {
754 pub user: User,
755 pub image: Option<String>,
756}
757
758/// The largest avatar that can be uploaded, in bytes.
759pub const MAX_AVATAR_BYTES: usize = 1024 * 1024;
760
Deploy scripts live in the repository761/// `list_workspace_tokens`: members only. Returns
762/// `Outcome<Vec<AccessToken>>`.
763#[derive(Debug, Serialize, Deserialize)]
764pub struct WorkspaceTokensArgs {
765 pub slug: String,
766 pub viewer: crate::Viewer,
767}
768
769/// `remove_workspace_token`: owners only. Returns `Outcome<bool>`.
770#[derive(Debug, Serialize, Deserialize)]
771pub struct RemoveWorkspaceTokenArgs {
772 pub actor: User,
773 pub slug: String,
774 pub id: String,
775}
776
777/// `oauth_authorize`: the signed-in person approved an application. The
778/// caller has checked the client and that it may be redirected to
779/// `redirect_uri`. Returns `OAuthCode`.
780#[derive(Debug, Serialize, Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct OAuthAuthorizeArgs {
783 pub user: User,
784 pub client_id: String,
785 /// Shown wherever the application's access is listed.
786 pub client_name: String,
787 pub redirect_uri: String,
788 /// PKCE challenge, method S256.
789 pub code_challenge: String,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step790 /// What the person granted, as `resource:level`. Null: full access.
791 #[serde(default)]
792 pub scopes: Option<Vec<String>>,
Deploy scripts live in the repository793}
794
795#[derive(Debug, Serialize, Deserialize)]
796pub struct OAuthCode {
797 pub code: String,
798}
799
800/// `oauth_exchange`: redeems an authorization code.
801/// Returns `Outcome<OAuthTokens>`.
802#[derive(Debug, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804pub struct OAuthExchangeArgs {
805 pub code: String,
806 pub code_verifier: String,
807 pub client_id: String,
808 pub redirect_uri: String,
809}
810
811/// `oauth_refresh`: trades a refresh token for new tokens.
812/// Returns `Outcome<OAuthTokens>`.
813#[derive(Debug, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815pub struct OAuthRefreshArgs {
816 pub refresh_token: String,
817 pub client_id: String,
818}
819
820#[derive(Debug, Serialize, Deserialize)]
821#[serde(rename_all = "camelCase")]
822pub struct OAuthTokens {
823 pub access_token: String,
824 /// Works once; using it returns the next one.
825 pub refresh_token: String,
826 /// Seconds until the access token stops working.
827 pub expires_in: u64,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step828 /// The scopes granted, space-separated, or `*` for full access.
829 #[serde(default)]
830 pub scope: Option<String>,
Deploy scripts live in the repository831}
832
833/// An application a person has signed in to. Listed by `list_oauth_grants`
834/// and ended by `revoke_oauth_grant`.
835#[derive(Debug, Serialize, Deserialize)]
836#[serde(rename_all = "camelCase")]
837pub struct OAuthGrant {
838 pub id: String,
839 pub client_name: String,
840 /// RFC 3339.
841 pub created_at: String,
842 /// RFC 3339.
843 pub last_used_at: String,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step844 /// What the person granted. Null: full access.
845 #[serde(default)]
846 pub scopes: Option<Vec<String>>,
847 /// Signed in before applications were given scopes: full access until
848 /// someone narrows it.
849 #[serde(default)]
850 pub legacy: bool,
851}
852
853/// `update_oauth_grant`: changes what an application the person signed in
854/// to may do, at once and when it refreshes. Returns `Outcome<OAuthGrant>`.
855#[derive(Debug, Serialize, Deserialize)]
856pub struct UpdateOAuthGrantArgs {
857 pub user: User,
858 pub id: String,
859 #[serde(default)]
860 pub scopes: Option<Vec<String>>,
Deploy scripts live in the repository861}
862
863
864/// What an agent's token may do: these operations, in this repository.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API865#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Deploy scripts live in the repository866pub struct AgentScope {
867 pub repo: crate::repos::RepoPath,
868 /// API and MCP operation names, such as `create_issue`.
869 pub operations: Vec<String>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API870 /// Set on a run credential: the run it belongs to, and what it may do
871 /// with git. See [`crate::credentials`].
872 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub run: Option<crate::credentials::RunBinding>,
Deploy scripts live in the repository874}
875
876/// `create_agent_token`: a token for a g1t agent working on someone's
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent877/// behalf. It acts as `g1t`, a member of the repository's workspace,
Deploy scripts live in the repository878/// and only for the operations in `scope`. Returns `CreatedAccessToken`.
879#[derive(Debug, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881pub struct CreateAgentTokenArgs {
882 /// The person the agent works for; the token is recorded as theirs.
883 pub on_behalf_of: User,
884 pub scope: AgentScope,
885 pub ttl_seconds: u64,
886}
887
888// `agent_scope` takes `TokenArgs` and returns `Option<AgentScope>`: what an
889// agent's token may do, or null for any other token.
890
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent891/// The id g1t's agent acts under. Only ever stored, never shown: it keeps
892/// the agent's work apart from g1t's own ([`crate::system::ID`]) where
893/// that matters, such as whether its approval counts.
Deploy scripts live in the repository894pub const AGENT_ID: &str = "usr_g1t_agent";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent895/// The name g1t's agent is shown by: g1t's own, [`crate::system::USERNAME`].
896/// Everything it does, people see g1t do.
897pub const AGENT_NAME: &str = crate::system::USERNAME;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace898
899// --- Staff ---------------------------------------------------------------
900//
901// Staff-only methods, for sudo.g1t.sh. They take no viewer and check no
902// membership: only sudo calls them, over its service binding, after it has
903// verified a Cloudflare Access sign-in and its staff list. Nothing a
904// customer can reach should ever forward to them.
905
906/// `notify_owners`: emails a short notice, with one link, to each owner of
907/// a workspace with a confirmed address. Called by other services (billing
908/// warns owners near their usage limit), never on a person's behalf.
909/// Returns how many were sent.
910#[derive(Clone, Debug, Serialize, Deserialize)]
911pub struct NotifyOwnersArgs {
912 pub workspace: String,
913 pub subject: String,
914 /// One or two sentences: what happened and what it means.
915 pub intro: String,
916 /// The button's words, such as `Open billing`.
917 pub action: String,
918 /// Where the button goes; must be on g1t.sh.
919 pub link: String,
920 /// Small print: why they got it.
921 pub footer: String,
922}
923
924/// `admin_workspaces`: every workspace, newest first, at most
925/// [`ADMIN_WORKSPACES_LIMIT`], optionally only those whose slug, name or
926/// an owner's username or email contains `query`. Returns
927/// `Vec<AdminWorkspace>`. Staff only.
928#[derive(Debug, Default, Serialize, Deserialize)]
929pub struct AdminWorkspacesArgs {
930 #[serde(default)]
931 pub query: Option<String>,
932}
933
934/// The most workspaces one `admin_workspaces` call returns.
935pub const ADMIN_WORKSPACES_LIMIT: usize = 500;
936
937/// An owner of a workspace, as staff see them.
938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
939pub struct AdminOwner {
940 pub username: String,
941 pub email: Option<String>,
942}
943
944/// A workspace as staff see it: who owns it and how many belong to it.
945#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
946#[serde(rename_all = "camelCase")]
947pub struct AdminWorkspace {
948 pub slug: String,
949 pub name: String,
950 /// RFC 3339.
951 pub created_at: String,
952 pub owners: Vec<AdminOwner>,
953 pub member_count: u32,
954}
955
956/// `admin_workspace`: one workspace with every member, or null. Takes
957/// `SlugArgs`; returns `Option<AdminWorkspaceDetail>`. Staff only.
958#[derive(Clone, Debug, Serialize, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct AdminWorkspaceDetail {
961 pub slug: String,
962 pub name: String,
963 pub description: Option<String>,
964 /// RFC 3339.
965 pub created_at: String,
966 /// Owners first, then by username.
967 pub members: Vec<AdminMember>,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member968 /// It can never be deleted ([`protected_names`]).
969 #[serde(default)]
970 pub protected: bool,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace971}
972
973/// A member of a workspace, as staff see them.
974#[derive(Clone, Debug, Serialize, Deserialize)]
975pub struct AdminMember {
976 pub username: String,
977 pub email: Option<String>,
978 pub role: crate::Role,
979 /// When they joined the workspace. RFC 3339.
980 pub joined: String,
981}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains982
983// --- Profiles ------------------------------------------------------------
984//
985// A person's public page at `g1t.sh/u/<username>`. Everything in a
986// `Profile` is shown to anyone, signed in or not; an email address never is.
987
988/// The most characters each profile field takes.
989pub const MAX_PROFILE_NAME: usize = 80;
990pub const MAX_PROFILE_BIO: usize = 160;
991pub const MAX_PROFILE_LOCATION: usize = 80;
992pub const MAX_PROFILE_WEBSITE: usize = 200;
993pub const MAX_PROFILE_PRONOUNS: usize = 40;
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)994pub const MAX_PROFILE_TIMEZONE: usize = 64;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains995
996/// What anyone may see about a person.
997#[derive(Clone, Debug, Default, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct Profile {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1000 /// Lowercased: what the profile is found and linked by.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1001 pub username: String,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1002 /// The username as its owner wrote it, when that differs: what the page shows.
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub display_username: Option<String>,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1005 /// The name they go by, if they gave one.
1006 pub name: Option<String>,
1007 /// One or two lines about them, at most [`MAX_PROFILE_BIO`] characters.
1008 pub bio: Option<String>,
1009 pub location: Option<String>,
1010 /// An `https://` address.
1011 pub website: Option<String>,
1012 pub pronouns: Option<String>,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)1013 /// The time zone they are in, an IANA name such as `America/Denver`.
1014 #[serde(default)]
1015 pub timezone: Option<String>,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1016 /// The uploaded avatar's hash, served at `/avatars/<avatar>`.
1017 pub avatar: Option<String>,
1018 /// When the account was made. RFC 3339.
1019 pub created_at: String,
1020}
1021
1022// `profile` takes `UsernameArgs` and returns `Option<Profile>`: null for
1023// an account that does not exist.
1024
1025/// `update_profile`: a person changes their own profile. Every field is
1026/// replaced; an empty one is cleared. Returns `Outcome<Profile>`.
1027#[derive(Debug, Default, Serialize, Deserialize)]
1028#[serde(rename_all = "camelCase")]
1029pub struct UpdateProfileArgs {
1030 pub actor: User,
1031 #[serde(default)]
1032 pub name: String,
1033 #[serde(default)]
1034 pub bio: String,
1035 #[serde(default)]
1036 pub location: String,
1037 #[serde(default)]
1038 pub website: String,
1039 #[serde(default)]
1040 pub pronouns: String,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)1041 /// An IANA time zone name, such as `America/Denver`.
1042 #[serde(default)]
1043 pub timezone: String,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1044}
1045
1046/// `profile_workspaces`: the workspaces shown on a person's profile, as
1047/// `viewer` may see them. A membership is shown only when it is no secret
1048/// from the viewer: a workspace the viewer belongs to as well, or one of
1049/// `public`, the workspaces the caller found the person has made a public
1050/// project in (whose page shows that already). Returns
1051/// `Vec<ProfileWorkspace>`; empty for an account that does not exist.
1052#[derive(Debug, Serialize, Deserialize)]
1053pub struct ProfileWorkspacesArgs {
1054 pub username: String,
1055 pub viewer: crate::Viewer,
1056 #[serde(default)]
1057 pub public: Vec<String>,
1058}
1059
1060/// A workspace on a person's profile.
1061#[derive(Clone, Debug, Serialize, Deserialize)]
1062pub struct ProfileWorkspace {
1063 pub slug: String,
1064 pub name: String,
1065 pub avatar: Option<String>,
1066}
Search across all of g1t, Explore, and a command palette1067
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.1068// --- Dock pins -------------------------------------------------------------
1069//
1070// The apps a person pins to their dock in a workspace, kept with their
1071// account so the dock follows them to every device. Each person's own:
1072// nobody else reads or sets them.
1073
1074/// The most apps a person pins in one workspace.
1075pub const MAX_DOCK_PINS: usize = 24;
1076/// The most characters an app key takes.
1077pub const MAX_DOCK_APP_KEY: usize = 32;
1078
1079/// `dock_pins`: the apps `user` pinned in the workspace `workspace` (a
1080/// slug), in the order they set. Returns `Option<Vec<String>>`: null when
1081/// they never saved any there, or are not one of its members.
1082#[derive(Debug, Default, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase")]
1084pub struct DockPinsArgs {
1085 pub user: User,
1086 pub workspace: String,
1087}
1088
1089/// `set_dock_pins`: replaces `user`'s pins in `workspace` with `apps`, in
1090/// that order. Each key is lowercase letters, digits and hyphens, at most
1091/// [`MAX_DOCK_APP_KEY`] characters; a repeat is dropped; at most
1092/// [`MAX_DOCK_PINS`]. Which keys name real apps is the web app's to say.
1093/// Returns `Outcome<Vec<String>>`: the pins as saved.
1094#[derive(Debug, Default, Serialize, Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct SetDockPinsArgs {
1097 pub user: User,
1098 pub workspace: String,
1099 #[serde(default)]
1100 pub apps: Vec<String>,
1101}
1102
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.1103// --- Sounds and desktop notifications --------------------------------------
1104//
1105// What a person hears when chat moves, kept with their account so a phone,
1106// a laptop and the desktop app all sound the same. Which sound plays when
1107// is the web app's rule (apps/web/app/lib/chat-sounds.ts); identity keeps
1108// the choices. Do not disturb is not here: it is the person's presence
1109// (`dnd_until`, services/notify), which also silences pop-ups and pushes.
1110// Mirrors `SoundSettings` in packages/contracts/src/identity.ts.
1111
1112/// The sets of sounds a person can choose from.
1113pub const SOUND_SETS: [&str; 2] = ["soft", "bright"];
1114/// The cues a person can turn on and off. `call` is reserved and has no setting.
1115pub const SOUND_CUES: [&str; 5] = ["message", "direct", "mention", "agent_done", "sent"];
1116/// Before anyone changes them: every cue but the tick as you send.
1117pub const SOUND_CUES_OFF_BY_DEFAULT: [&str; 1] = ["sent"];
1118pub const DEFAULT_SOUND_SET: &str = "soft";
1119pub const DEFAULT_SOUND_VOLUME: u8 = 60;
1120
1121/// A person's sound settings, as kept and as answered. Wire fields are
1122/// snake_case, as the site's JSON is.
1123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1124pub struct SoundSettings {
1125 /// Whether anything plays at all.
1126 pub sounds_enabled: bool,
1127 /// One of [`SOUND_SETS`].
1128 pub sound_set: String,
1129 /// 0 to 100.
1130 pub sound_volume: u8,
1131 /// Each cue on or off, by name; a cue left out is on, except `sent`.
1132 #[serde(default)]
1133 pub sound_cues: std::collections::BTreeMap<String, bool>,
1134 /// Whether an open tab shows a system notification for a message to
1135 /// you while its window is not in front.
1136 pub desktop_toasts: bool,
1137}
1138
1139impl Default for SoundSettings {
1140 fn default() -> Self {
1141 SoundSettings {
1142 sounds_enabled: true,
1143 sound_set: DEFAULT_SOUND_SET.to_owned(),
1144 sound_volume: DEFAULT_SOUND_VOLUME,
1145 sound_cues: SOUND_CUES
1146 .iter()
1147 .map(|cue| ((*cue).to_owned(), !SOUND_CUES_OFF_BY_DEFAULT.contains(cue)))
1148 .collect(),
1149 desktop_toasts: false,
1150 }
1151 }
1152}
1153
1154/// A change to them: only what is sent changes; `sound_cues` merges by cue.
1155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct SoundSettingsChange {
1157 #[serde(default)]
1158 pub sounds_enabled: Option<bool>,
1159 #[serde(default)]
1160 pub sound_set: Option<String>,
1161 #[serde(default)]
1162 pub sound_volume: Option<i64>,
1163 #[serde(default)]
1164 pub sound_cues: Option<std::collections::BTreeMap<String, bool>>,
1165 #[serde(default)]
1166 pub desktop_toasts: Option<bool>,
1167}
1168
1169/// `sound_settings`: `user`'s own, or the defaults when they never changed
1170/// them. Returns `SoundSettings`. People only.
1171#[derive(Debug, Default, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct SoundSettingsArgs {
1174 pub user: User,
1175}
1176
1177/// `set_sound_settings`: changes `user`'s own. Returns
1178/// `Outcome<SoundSettings>`: the settings as kept, or `invalid` for a set
1179/// or cue g1t does not have, or a volume outside 0 to 100.
1180#[derive(Debug, Default, Serialize, Deserialize)]
1181#[serde(rename_all = "camelCase")]
1182pub struct SetSoundSettingsArgs {
1183 pub user: User,
1184 #[serde(default)]
1185 pub change: SoundSettingsChange,
1186}
1187
Search across all of g1t, Explore, and a command palette1188/// `directory`: every account or every workspace, as their public pages
1189/// show them, a page at a time in name order. For services that index
1190/// them, such as search; nothing private is in it. Returns
1191/// `DirectoryPage`.
1192#[derive(Debug, Default, Serialize, Deserialize)]
1193pub struct DirectoryArgs {
1194 /// `user` or `workspace`.
1195 pub kind: String,
1196 /// Names after this one.
1197 #[serde(default)]
1198 pub after: Option<String>,
1199 pub limit: u32,
1200}
1201
1202/// One account or workspace in the directory.
1203#[derive(Clone, Debug, Serialize, Deserialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct DirectoryEntry {
1206 /// The account's or workspace's id.
1207 pub id: String,
1208 /// A username or a workspace's slug.
1209 pub slug: String,
1210 /// A person's display name or a workspace's name.
1211 pub name: Option<String>,
1212 /// A person's bio or a workspace's description.
1213 pub bio: Option<String>,
1214 pub avatar: Option<String>,
1215 /// RFC 3339.
1216 pub created_at: String,
1217}
1218
1219#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1220pub struct DirectoryPage {
1221 pub entries: Vec<DirectoryEntry>,
1222 /// Where the next page starts; null on the last.
1223 pub next: Option<String>,
1224}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1225
1226// --- Invites ---------------------------------------------------------------
1227//
1228// While registration is invite-only, every new account (with a password or
1229// through GitHub) needs an invite code. Each person may have
1230// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1231// to a workspace, whose owners share them. Inviting an email with no
1232// account into a workspace makes an invite bound to that address, which
1233// registers and joins in one step. See services/identity/src/invites.rs.
1234
1235/// Whether anyone may make an account, or only someone with an invite. Set
1236/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1237/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1239#[serde(rename_all = "snake_case")]
1240pub enum RegistrationMode {
1241 #[default]
1242 Invite,
1243 Open,
1244}
1245
1246impl RegistrationMode {
1247 pub fn parse(text: Option<&str>) -> RegistrationMode {
1248 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1249 Some("open") => RegistrationMode::Open,
1250 _ => RegistrationMode::Invite,
1251 }
1252 }
1253}
1254
1255/// How many invites a person may have out at once, unless identity's
1256/// `INVITES_PER_USER` var says otherwise.
1257pub const INVITES_PER_USER: u32 = 5;
1258
1259/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1260/// otherwise.
1261pub const INVITE_TTL_DAYS: u64 = 30;
1262
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1263/// Where an invite stands. Only a pending invite can be used. A pending
1264/// invite can be revoked, and so can one awaiting confirmation. An expired
1265/// or revoked invite that was never used gives its inviter the invite back.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1267#[serde(rename_all = "snake_case")]
1268pub enum InviteStatus {
1269 Pending,
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1270 /// Used to make an account that has not confirmed its email address
1271 /// yet. The code is spent; the workspace (or repository) it gives is
1272 /// joined when the address is confirmed, unless it is revoked first.
1273 AwaitingConfirmation,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1274 /// Used to make an account that has confirmed its address, for a
1275 /// workspace it has not yet joined or declined: the workspace
1276 /// invitation waits for the person's answer (`accept_invitation`).
1277 AwaitingAnswer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1278 Redeemed,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1279 /// Its person declined the workspace it invited them to.
1280 Declined,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1281 Expired,
1282 Revoked,
1283}
1284
1285/// What using an invite does.
1286#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1287#[serde(rename_all = "snake_case")]
1288pub enum InviteKind {
1289 /// Makes a new account, and joins `workspace` when one is set.
1290 Account,
1291 /// An existing account joins `workspace`. Never makes an account.
1292 Workspace,
1293}
1294
1295/// Whose allowance an invite uses.
1296#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1297#[serde(rename_all = "snake_case")]
1298pub enum InviteCharge {
1299 /// Its inviter's own.
1300 User,
1301 /// The workspace's, granted by staff and shared by its owners.
1302 Workspace,
1303 /// Nobody's: staff minted it, or it invites an existing account.
1304 None,
1305}
1306
1307/// One invite, as the person who made it sees it.
1308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1309#[serde(rename_all = "camelCase")]
1310pub struct Invite {
1311 pub id: String,
1312 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1313 /// is made, and afterwards to whoever made it while it is pending.
1314 /// Null otherwise.
1315 pub code: Option<String>,
1316 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1317 pub hint: String,
1318 /// Only an account with this address can use it. Null: anyone with
1319 /// the code.
1320 pub email: Option<String>,
1321 pub kind: InviteKind,
1322 /// The workspace it joins, by slug.
1323 pub workspace: Option<String>,
1324 pub status: InviteStatus,
1325 pub charged_to: InviteCharge,
1326 /// Who made it, by username. Null when g1t staff did.
1327 pub invited_by: Option<String>,
1328 /// The account that used it, by username.
1329 pub redeemed_by: Option<String>,
1330 /// RFC 3339.
1331 pub created_at: String,
1332 /// RFC 3339.
1333 pub expires_at: String,
1334 /// RFC 3339.
1335 pub redeemed_at: Option<String>,
1336 /// RFC 3339.
1337 pub revoked_at: Option<String>,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1338 /// The account a workspace invitation is for, by username: someone
1339 /// invited by username, or the account the invite made.
1340 #[serde(default)]
1341 pub invitee: Option<String>,
1342 /// The role `workspace` is joined with. Null when it names none.
1343 #[serde(default)]
1344 pub role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1345 /// The staff member who minted it. Only in staff views.
1346 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub staff: Option<String>,
1348}
1349
1350/// How many invites someone may have out, and how many they have.
1351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1352pub struct Allowance {
1353 /// Null: no limit.
1354 pub limit: Option<u32>,
1355 /// Pending and used invites; revoked and expired ones are not counted.
1356 pub used: u32,
1357 /// Null: no limit.
1358 pub remaining: Option<u32>,
1359}
1360
1361impl Allowance {
1362 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1363 Allowance {
1364 limit,
1365 used,
1366 remaining: limit.map(|limit| limit.saturating_sub(used)),
1367 }
1368 }
1369
1370 pub fn exhausted(&self) -> bool {
1371 self.remaining == Some(0)
1372 }
1373}
1374
1375/// A workspace's shared invites, for one of its owners.
1376#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1377pub struct WorkspaceAllowance {
1378 pub slug: String,
1379 pub allowance: Allowance,
1380}
1381
1382/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1383/// and what they have left. Returns `InvitesOverview`.
1384#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1385pub struct InvitesOverview {
1386 pub mode: RegistrationMode,
1387 pub allowance: Allowance,
1388 /// Workspaces the person owns that staff granted invites to.
1389 pub workspaces: Vec<WorkspaceAllowance>,
1390 pub invites: Vec<Invite>,
1391}
1392
1393/// `create_invite`: a person makes an invite, optionally for one email
1394/// address. People only; never an agent or a workspace's token, and not
1395/// before their email is confirmed. Uses one of the person's invites, or,
1396/// with `workspace`, one of the invites staff granted that workspace (its
1397/// owners only). Emails the address when one is given. Returns
1398/// `Outcome<Invite>`, with the code.
1399///
1400/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1401/// invite; a workspace's owners may revoke one made for the workspace.
1402/// The invite comes back to whoever it was charged to. Returns
1403/// `Outcome<Invite>`.
1404#[derive(Debug, Serialize, Deserialize)]
1405pub struct CreateInviteArgs {
1406 pub user: User,
1407 #[serde(default)]
1408 pub email: Option<String>,
1409 /// Use this workspace's granted invites, by slug.
1410 #[serde(default)]
1411 pub workspace: Option<String>,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1412 /// The workspace the new account is invited to, by slug: one the
1413 /// person owns that can add members (not on the free plan). Once the
1414 /// account is confirmed it gets a workspace invitation to accept, as a
1415 /// member, and no workspace of its own is made for it.
1416 #[serde(default)]
1417 pub join: Option<String>,
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)1418 /// The role `join` invites them with; member when absent. Ignored
1419 /// without `join`.
1420 #[serde(default)]
1421 pub join_role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1422 /// Where the request came in, for the audit log; g1t.sh when absent.
1423 #[serde(default)]
1424 pub surface: Option<crate::audit::Surface>,
1425}
1426
1427/// `check_invite`: what an invite code is for, before using it. Returns
1428/// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1429/// expired gets the same answer, so codes cannot be probed. With
1430/// `any_status`, a real code that can no longer be used is described
1431/// instead (its `status` says why), so the page can say whom to ask for a
1432/// new one; an unknown code still gets the one answer.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1433#[derive(Debug, Serialize, Deserialize)]
1434pub struct InviteCodeArgs {
1435 pub code: String,
1436 /// Who is asking, such as the visitor's IP address, for rate limits.
1437 #[serde(default)]
1438 pub client: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1439 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1440 #[serde(default)]
1441 pub viewer: Option<User>,
1442 #[serde(default)]
1443 pub any_status: bool,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm1444 /// The `proof` from the invite email's link, if the page was opened
1445 /// from it: sets `InvitePreview::email_proven`.
1446 #[serde(default)]
1447 pub email_proof: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1448}
1449
1450/// Someone shown on an invite.
1451#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1452pub struct InviteFrom {
1453 pub username: String,
1454 pub name: Option<String>,
1455 pub avatar: Option<String>,
1456}
1457
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1458/// A repository an invite code was sent with: using the code accepts the
1459/// invitation to collaborate on it.
1460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1461pub struct InviteRepository {
1462 /// `workspace/repo`.
1463 pub name: String,
1464 /// The role it gives, such as `write`.
1465 pub role: String,
1466}
1467
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1468/// What a valid invite code is for.
1469#[derive(Clone, Debug, Serialize, Deserialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct InvitePreview {
1472 pub kind: InviteKind,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1473 /// Pending, unless `any_status` asked about a code that is spent.
1474 pub status: InviteStatus,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1475 /// Null when g1t staff sent it.
1476 pub invited_by: Option<InviteFrom>,
1477 pub workspace: Option<ProfileWorkspace>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1478 /// The repository it accepts an invitation to, if it was sent with one.
1479 pub repository: Option<InviteRepository>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1480 /// The address it is for, partly hidden, such as `a•••@example.com`.
1481 pub email: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1482 /// The address in full, while it is pending: whoever holds the code
1483 /// was sent it there. Fills in and locks the sign-up form.
1484 pub address: Option<String>,
1485 /// Whether the address it is for has a g1t account already, so the
1486 /// page asks them to sign in rather than sign up.
1487 pub has_account: bool,
1488 /// With a viewer: whether the invite is theirs (it is for one of their
1489 /// confirmed addresses, or they used it). Null without a viewer or,
1490 /// for a pending invite, when it is for anyone with the code.
1491 pub for_viewer: Option<bool>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1492 /// RFC 3339.
1493 pub expires_at: String,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1494 /// For a shared invite link ([`SharedInvite`]): the group it was made
1495 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1496 /// it. Null for a one-person invite.
1497 #[serde(default)]
1498 pub shared_label: Option<String>,
1499 /// For a shared invite link limited to some email domains: those
1500 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1501 #[serde(default)]
1502 pub shared_domains: Vec<String>,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm1503 /// Whether `email_proof` was this pending invite's own, from the email
1504 /// it was sent in: the account made with it starts with `address`
1505 /// confirmed. False without a proof, with a wrong one, and for an
1506 /// invite bound to no address.
1507 #[serde(default)]
1508 pub email_proven: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1509}
1510
1511/// `accept_invite`: a signed-in person uses a workspace invite made for
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1512/// their confirmed address, and joins the workspace, or an invite sent with
1513/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1514/// workspace's slug, or `workspace/repo`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1515#[derive(Debug, Serialize, Deserialize)]
1516pub struct AcceptInviteArgs {
1517 pub user: User,
1518 pub code: String,
1519}
1520
1521/// `invite_member`: an owner invites an email address into a workspace.
1522/// It always makes an invite bound to that address and emails it, so the
1523/// answer never says whether the address has an account. Without one, the
1524/// invite registers and joins in one step, and uses one of the workspace's
1525/// granted invites or else one of the owner's own. With one, it costs
1526/// nothing. Returns `Outcome<Invite>`, with the code.
1527#[derive(Debug, Serialize, Deserialize)]
1528pub struct InviteMemberArgs {
1529 pub actor: User,
1530 pub slug: String,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1531 /// An email address. Give this or `username`.
1532 #[serde(default)]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1533 pub email: String,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1534 /// A g1t username: that account gets a workspace invitation to accept
1535 /// or decline, in its inbox and by email. Nobody joins without saying
1536 /// yes.
1537 #[serde(default)]
1538 pub username: Option<String>,
1539 /// The role they join with; member when absent.
1540 #[serde(default)]
1541 pub role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1542 /// Where the request came in, for the audit log; g1t.sh when absent.
1543 #[serde(default)]
1544 pub surface: Option<crate::audit::Surface>,
1545}
1546
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1547/// A workspace invitation waiting for its person's answer, as they see it.
1548/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1549/// newest first: pending ones only, never expired, revoked or answered.
1550#[derive(Clone, Debug, Serialize, Deserialize)]
1551#[serde(rename_all = "camelCase")]
1552pub struct WorkspaceInvitation {
1553 pub id: String,
1554 pub workspace: ProfileWorkspace,
1555 /// The role accepting joins with.
1556 pub role: crate::Role,
1557 /// Null when g1t staff sent it.
1558 pub invited_by: Option<InviteFrom>,
1559 /// RFC 3339.
1560 pub created_at: String,
1561 /// RFC 3339.
1562 pub expires_at: String,
1563}
1564
1565/// `accept_invitation`: the person it is for joins the workspace with the
1566/// role it names. Returns `Outcome<String>`, the workspace's slug.
1567///
1568/// `decline_invitation`: they say no; whoever sent it is told in their
1569/// inbox. Returns `Outcome<bool>`.
1570#[derive(Debug, Serialize, Deserialize)]
1571pub struct InvitationArgs {
1572 pub user: User,
1573 pub id: String,
1574 /// Where the request came in, for the audit log; g1t.sh when absent.
1575 #[serde(default)]
1576 pub surface: Option<crate::audit::Surface>,
1577}
1578
1579/// `find_people`: accounts whose username starts with `query`, or whose
1580/// name contains it, for picking someone to invite. Only what a profile
1581/// shows: a username, a name and an avatar, never an email address.
1582/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1583#[derive(Debug, Serialize, Deserialize)]
1584pub struct FindPeopleArgs {
1585 pub query: String,
1586 #[serde(default)]
1587 pub limit: Option<u32>,
1588}
1589
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1590/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1591/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1592///
1593/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1594#[derive(Debug, Serialize, Deserialize)]
1595pub struct WorkspaceInviteArgs {
1596 pub actor: User,
1597 pub slug: String,
1598 pub id: String,
1599}
1600
1601/// `request_access`: someone without an invite asks for one. Kept on the
1602/// waitlist, one entry per address. Answers the same way whether or not
1603/// the address is already on it. Returns `Outcome<bool>`.
1604#[derive(Debug, Default, Serialize, Deserialize)]
1605pub struct RequestAccessArgs {
1606 pub email: String,
1607 /// What they will build, if they said.
1608 #[serde(default)]
1609 pub about: String,
1610 /// Who is asking, such as the visitor's IP address, for rate limits.
1611 #[serde(default)]
1612 pub client: Option<String>,
1613}
1614
1615/// The most characters `RequestAccessArgs::about` keeps.
1616pub const MAX_WAITLIST_ABOUT: usize = 1000;
1617
1618// `registration` takes `{}` and returns `RegistrationMode`.
1619
1620// --- Invites, staff only ---
1621
1622#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1623#[serde(rename_all = "snake_case")]
1624pub enum WaitlistStatus {
1625 Waiting,
1626 Invited,
1627 Dismissed,
1628}
1629
1630impl WaitlistStatus {
1631 pub fn as_str(self) -> &'static str {
1632 match self {
1633 WaitlistStatus::Waiting => "waiting",
1634 WaitlistStatus::Invited => "invited",
1635 WaitlistStatus::Dismissed => "dismissed",
1636 }
1637 }
1638}
1639
1640/// Someone who asked for access.
1641#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1642#[serde(rename_all = "camelCase")]
1643pub struct WaitlistEntry {
1644 pub id: String,
1645 pub email: String,
1646 pub about: Option<String>,
1647 pub status: WaitlistStatus,
1648 pub invite_id: Option<String>,
1649 pub decided_by: Option<String>,
1650 /// RFC 3339.
1651 pub decided_at: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1652 /// What staff wrote when approving; it went in the invite email.
1653 #[serde(default)]
1654 pub note: Option<String>,
1655 /// The account made with the invite, once it was used.
1656 #[serde(default)]
1657 pub joined_as: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1658 /// When they first asked. RFC 3339.
1659 pub created_at: String,
1660 /// When they last asked. RFC 3339.
1661 pub updated_at: String,
1662}
1663
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1664/// `admin_waitlist`: the waitlist, newest first, at most
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1665/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1666///
1667/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1668/// still waiting, for sudo's navigation.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1669#[derive(Debug, Default, Serialize, Deserialize)]
1670pub struct AdminWaitlistArgs {
1671 /// Part of an email address or of what they said.
1672 #[serde(default)]
1673 pub query: Option<String>,
1674 /// Null: every status.
1675 #[serde(default)]
1676 pub status: Option<WaitlistStatus>,
1677}
1678
1679/// The most rows one staff listing of invites or the waitlist returns.
1680pub const ADMIN_INVITES_LIMIT: usize = 500;
1681
1682/// `admin_decide_waitlist`: approving mints an invite bound to the
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1683/// address, charged to nobody, and emails it, with `note` if given;
1684/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1685#[derive(Debug, Serialize, Deserialize)]
1686pub struct AdminDecideWaitlistArgs {
1687 pub id: String,
1688 pub approve: bool,
1689 /// The staff member, by email.
1690 pub staff: String,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1691 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1692 #[serde(default)]
1693 pub note: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1694}
1695
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1696/// The most characters an approval's note keeps.
1697pub const MAX_WAITLIST_NOTE: usize = 500;
1698
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1699/// `admin_invites`: every invite, newest first, at most
1700/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1701/// `query`, or whose email, inviter or redeemer contains it. Returns
1702/// `Vec<Invite>`.
1703#[derive(Debug, Default, Serialize, Deserialize)]
1704pub struct AdminInvitesArgs {
1705 #[serde(default)]
1706 pub query: Option<String>,
1707}
1708
1709/// `admin_revoke_invite`: revokes any pending invite. Returns
1710/// `Outcome<Invite>`.
1711#[derive(Debug, Serialize, Deserialize)]
1712pub struct AdminRevokeInviteArgs {
1713 pub id: String,
1714 pub staff: String,
1715}
1716
1717/// `admin_mint_invite`: staff make an invite that uses nobody's
1718/// allowance, optionally bound to (and emailed to) an address. Returns
1719/// `Outcome<Invite>`, with the code.
1720#[derive(Debug, Serialize, Deserialize)]
1721pub struct AdminMintInviteArgs {
1722 #[serde(default)]
1723 pub email: Option<String>,
1724 pub staff: String,
1725}
1726
1727/// Who staff grant invites to.
1728#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1729#[serde(rename_all = "snake_case")]
1730pub enum GrantTarget {
1731 User,
1732 Workspace,
1733}
1734
1735impl GrantTarget {
1736 pub fn as_str(self) -> &'static str {
1737 match self {
1738 GrantTarget::User => "user",
1739 GrantTarget::Workspace => "workspace",
1740 }
1741 }
1742}
1743
1744/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1745/// slug) `amount` more invites; a negative amount takes some back. Returns
1746/// `Outcome<Allowance>`: theirs afterwards.
1747#[derive(Debug, Serialize, Deserialize)]
1748pub struct AdminGrantInvitesArgs {
1749 pub target: GrantTarget,
1750 pub name: String,
1751 pub amount: i32,
1752 #[serde(default)]
1753 pub note: String,
1754 pub staff: String,
1755}
1756
1757/// The most invites one grant gives or takes back.
1758pub const MAX_INVITE_GRANT: i32 = 1000;
1759
1760/// Invites staff granted.
1761#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1762#[serde(rename_all = "camelCase")]
1763pub struct InviteGrant {
1764 pub amount: i32,
1765 pub note: Option<String>,
1766 pub granted_by: String,
1767 /// RFC 3339.
1768 pub created_at: String,
1769}
1770
1771/// Someone a person invited, and whom they invited in turn.
1772#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1773#[serde(rename_all = "camelCase")]
1774pub struct InviteTreeNode {
1775 pub username: String,
1776 /// When they used the invite. RFC 3339.
1777 pub joined_at: String,
1778 pub invited: Vec<InviteTreeNode>,
1779}
1780
1781/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1782/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1783///
1784/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1785/// invites, grants and invites. Returns `Option<InviteTree>` with
1786/// `username` the slug and no `invited_by`.
1787#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1788#[serde(rename_all = "camelCase")]
1789pub struct InviteTree {
1790 pub username: String,
1791 /// Who invited them, then who invited that person, and so on. Empty
1792 /// for an account made without an invite.
1793 pub invited_by: Vec<String>,
1794 /// The staff member who minted their invite, when staff did.
1795 pub staff: Option<String>,
1796 pub allowance: Allowance,
1797 pub grants: Vec<InviteGrant>,
1798 /// Their invites, newest first.
1799 pub invites: Vec<Invite>,
1800 /// Whom they invited, three levels down.
1801 pub invited: Vec<InviteTreeNode>,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1802 /// The shared invite link the account was made with, if it was.
1803 #[serde(default)]
1804 pub shared: Option<SharedInviteSource>,
1805}
1806
1807// --- Shared invite links, staff only ---
1808//
1809// One link for a group (a conference's judges, a post, a community): up
1810// to `max_uses` new accounts, until it expires or staff revoke it,
1811// optionally only for addresses at some domains. Each use makes a new
1812// account, which then makes its own workspace; a shared link never joins
1813// anyone to an existing workspace, and uses nobody's allowance. Its code
1814// looks and is stored like any invite code (only a hash, and a sealed copy
1815// staff can copy again while it is live); the link is
1816// `https://g1t.sh/register?invite=<code>`. See
1817// services/identity/src/shared_invites.rs.
1818
1819/// How long a shared invite link works when staff give no date.
1820pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1821/// The furthest ahead a shared invite link's last day may be set.
1822pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1823/// The most accounts one shared invite link makes.
1824pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1825/// The most characters a shared invite link's label keeps.
1826pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1827/// The most email domains one shared invite link may be limited to.
1828pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1829
1830/// Where a shared invite link stands. Only a live one makes accounts.
1831#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1832#[serde(rename_all = "snake_case")]
1833pub enum SharedInviteStatus {
1834 Live,
1835 /// Every use is taken.
1836 UsedUp,
1837 Expired,
1838 Revoked,
1839}
1840
1841/// The shared invite link an account was made with.
1842#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1843pub struct SharedInviteSource {
1844 pub id: String,
1845 pub label: String,
1846}
1847
1848/// An account made with a shared invite link.
1849#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1850#[serde(rename_all = "camelCase")]
1851pub struct SharedInviteAccount {
1852 /// Null once the account is purged.
1853 pub username: Option<String>,
1854 /// When it was made with the link. RFC 3339.
1855 pub joined_at: String,
1856}
1857
1858/// One shared invite link, as staff see it.
1859#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1860#[serde(rename_all = "camelCase")]
1861pub struct SharedInvite {
1862 /// `sinv_…`.
1863 pub id: String,
1864 /// Whom it is for, such as `Cloudflare judges`.
1865 pub label: String,
1866 /// The code, while it is live (and IDENTITY_KEY is set).
1867 pub code: Option<String>,
1868 /// The code's first group, such as `g1t-k7m2`.
1869 pub hint: String,
1870 pub max_uses: u32,
1871 /// Accounts made with it so far.
1872 pub uses: u32,
1873 /// Only addresses at these domains may use it; empty for any.
1874 pub domains: Vec<String>,
1875 pub status: SharedInviteStatus,
1876 /// The staff member who made it, by email.
1877 pub staff: String,
1878 /// RFC 3339.
1879 pub created_at: String,
1880 /// RFC 3339.
1881 pub expires_at: String,
1882 pub revoked_at: Option<String>,
1883 pub revoked_by: Option<String>,
1884 /// The accounts made with it, oldest first.
1885 pub accounts: Vec<SharedInviteAccount>,
1886}
1887
1888/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1889/// at most 200, each with the accounts it made. Returns
1890/// `Vec<SharedInvite>`.
1891///
1892/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1893/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1894#[derive(Debug, Default, Serialize, Deserialize)]
1895pub struct AdminCreateSharedInviteArgs {
1896 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1897 pub label: String,
1898 /// 1 to [`MAX_SHARED_INVITE_USES`].
1899 pub max_uses: u32,
1900 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1901 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1902 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1903 #[serde(default)]
1904 pub expires_on: Option<String>,
1905 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1906 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1907 #[serde(default)]
1908 pub domains: Vec<String>,
1909 /// The staff member, by email.
1910 pub staff: String,
1911}
1912
1913/// `admin_revoke_shared_invite`: stops a shared invite link making any
1914/// more accounts. Those it made stay. Recorded in sudo's audit log.
1915/// Returns `Outcome<SharedInvite>`.
1916#[derive(Debug, Serialize, Deserialize)]
1917pub struct AdminRevokeSharedInviteArgs {
1918 pub id: String,
1919 pub staff: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1920}
1921
1922#[cfg(test)]
1923mod deletion_tests {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1924 use super::{WorkspaceDeletion, protected_names};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1925
1926 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1927 fn only_billing_or_protection_stands_in_the_way() {
1928 let clear = WorkspaceDeletion {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1929 repositories: 2,
1930 projects: 1,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1931 members: 3,
1932 ..WorkspaceDeletion::default()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1933 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1934 assert!(!clear.blocked());
1935 assert_eq!(clear.reason("acme"), None);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1936 let owing = WorkspaceDeletion {
1937 billing: Some("Pay first.".into()),
1938 ..WorkspaceDeletion::default()
1939 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1940 assert!(owing.blocked());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1941 assert_eq!(owing.reason("acme").as_deref(), Some("Pay first."));
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1942 let protected = WorkspaceDeletion {
1943 billing: Some("Pay first.".into()),
1944 protected: true,
1945 ..WorkspaceDeletion::default()
1946 };
1947 assert!(protected.blocked());
1948 assert_eq!(
1949 protected.reason("flagon-io").as_deref(),
1950 Some("flagon-io is protected and can never be deleted.")
1951 );
1952 }
1953
1954 #[test]
1955 fn flagon_is_protected_whatever_the_variable_says() {
1956 assert_eq!(protected_names(None), ["flagon-io"]);
1957 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1958 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1959 assert_eq!(
1960 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1961 ["flagon-io", "acme", "wsp_1"]
1962 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1963 }
1964}

This file's history is long; its oldest lines are credited to the oldest commit read.