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

Webhooks: every event, to your own addresses, signed and retried1//! 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>,
Webhooks: every event, to your own addresses, signed and retried22}
23
Fine-grained personal tokens, workspace token rules and approvals in identity24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
Webhooks: every event, to your own addresses, signed and retried25#[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,
Webhooks: every event, to your own addresses, signed and retried85}
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`).
Webhooks: every event, to your own addresses, signed and retried91/// 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>,
Webhooks: every event, to your own addresses, signed and retried99}
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.
Webhooks: every event, to your own addresses, signed and retried107 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>,
Webhooks: every event, to your own addresses, signed and retried113}
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`]).
Webhooks: every event, to your own addresses, signed and retried136#[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>`).
Webhooks: every event, to your own addresses, signed and retried151#[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
Webhooks: every event, to your own addresses, signed and retried165/// `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
Webhooks: every event, to your own addresses, signed and retried180/// `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.
Webhooks: every event, to your own addresses, signed and retried208 #[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
Webhooks: every event, to your own addresses, signed and retried251/// 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`].
Webhooks: every event, to your own addresses, signed and retried264#[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>,
Webhooks: every event, to your own addresses, signed and retried281}
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.
Webhooks: every event, to your own addresses, signed and retried294#[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>,
Webhooks: every event, to your own addresses, signed and retried300}
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,
Webhooks: every event, to your own addresses, signed and retried412}
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>,
Webhooks: every event, to your own addresses, signed and retried421 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>,
Webhooks: every event, to your own addresses, signed and retried436}
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
Webhooks: every event, to your own addresses, signed and retried477/// `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>>`.
Webhooks: every event, to your own addresses, signed and retried494#[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>`.
Webhooks: every event, to your own addresses, signed and retried506#[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>,
Webhooks: every event, to your own addresses, signed and retried513}
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
Webhooks: every event, to your own addresses, signed and retried761/// `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>>,
Webhooks: every event, to your own addresses, signed and retried793}
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>,
Webhooks: every event, to your own addresses, signed and retried831}
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>>,
Webhooks: every event, to your own addresses, signed and retried861}
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)]
Webhooks: every event, to your own addresses, signed and retried866pub 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>,
Webhooks: every event, to your own addresses, signed and retried874}
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,
Webhooks: every event, to your own addresses, signed and retried878/// 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.
Webhooks: every event, to your own addresses, signed and retried894pub 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
Search across all of g1t, Explore, and a command palette1103/// `directory`: every account or every workspace, as their public pages
1104/// show them, a page at a time in name order. For services that index
1105/// them, such as search; nothing private is in it. Returns
1106/// `DirectoryPage`.
1107#[derive(Debug, Default, Serialize, Deserialize)]
1108pub struct DirectoryArgs {
1109 /// `user` or `workspace`.
1110 pub kind: String,
1111 /// Names after this one.
1112 #[serde(default)]
1113 pub after: Option<String>,
1114 pub limit: u32,
1115}
1116
1117/// One account or workspace in the directory.
1118#[derive(Clone, Debug, Serialize, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub struct DirectoryEntry {
1121 /// The account's or workspace's id.
1122 pub id: String,
1123 /// A username or a workspace's slug.
1124 pub slug: String,
1125 /// A person's display name or a workspace's name.
1126 pub name: Option<String>,
1127 /// A person's bio or a workspace's description.
1128 pub bio: Option<String>,
1129 pub avatar: Option<String>,
1130 /// RFC 3339.
1131 pub created_at: String,
1132}
1133
1134#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1135pub struct DirectoryPage {
1136 pub entries: Vec<DirectoryEntry>,
1137 /// Where the next page starts; null on the last.
1138 pub next: Option<String>,
1139}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1140
1141// --- Invites ---------------------------------------------------------------
1142//
1143// While registration is invite-only, every new account (with a password or
1144// through GitHub) needs an invite code. Each person may have
1145// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1146// to a workspace, whose owners share them. Inviting an email with no
1147// account into a workspace makes an invite bound to that address, which
1148// registers and joins in one step. See services/identity/src/invites.rs.
1149
1150/// Whether anyone may make an account, or only someone with an invite. Set
1151/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1152/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1154#[serde(rename_all = "snake_case")]
1155pub enum RegistrationMode {
1156 #[default]
1157 Invite,
1158 Open,
1159}
1160
1161impl RegistrationMode {
1162 pub fn parse(text: Option<&str>) -> RegistrationMode {
1163 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1164 Some("open") => RegistrationMode::Open,
1165 _ => RegistrationMode::Invite,
1166 }
1167 }
1168}
1169
1170/// How many invites a person may have out at once, unless identity's
1171/// `INVITES_PER_USER` var says otherwise.
1172pub const INVITES_PER_USER: u32 = 5;
1173
1174/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1175/// otherwise.
1176pub const INVITE_TTL_DAYS: u64 = 30;
1177
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1178/// Where an invite stands. Only a pending invite can be used. A pending
1179/// invite can be revoked, and so can one awaiting confirmation. An expired
1180/// 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 look1181#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1182#[serde(rename_all = "snake_case")]
1183pub enum InviteStatus {
1184 Pending,
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1185 /// Used to make an account that has not confirmed its email address
1186 /// yet. The code is spent; the workspace (or repository) it gives is
1187 /// joined when the address is confirmed, unless it is revoked first.
1188 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)1189 /// Used to make an account that has confirmed its address, for a
1190 /// workspace it has not yet joined or declined: the workspace
1191 /// invitation waits for the person's answer (`accept_invitation`).
1192 AwaitingAnswer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1193 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)1194 /// Its person declined the workspace it invited them to.
1195 Declined,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1196 Expired,
1197 Revoked,
1198}
1199
1200/// What using an invite does.
1201#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1202#[serde(rename_all = "snake_case")]
1203pub enum InviteKind {
1204 /// Makes a new account, and joins `workspace` when one is set.
1205 Account,
1206 /// An existing account joins `workspace`. Never makes an account.
1207 Workspace,
1208}
1209
1210/// Whose allowance an invite uses.
1211#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1212#[serde(rename_all = "snake_case")]
1213pub enum InviteCharge {
1214 /// Its inviter's own.
1215 User,
1216 /// The workspace's, granted by staff and shared by its owners.
1217 Workspace,
1218 /// Nobody's: staff minted it, or it invites an existing account.
1219 None,
1220}
1221
1222/// One invite, as the person who made it sees it.
1223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1224#[serde(rename_all = "camelCase")]
1225pub struct Invite {
1226 pub id: String,
1227 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1228 /// is made, and afterwards to whoever made it while it is pending.
1229 /// Null otherwise.
1230 pub code: Option<String>,
1231 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1232 pub hint: String,
1233 /// Only an account with this address can use it. Null: anyone with
1234 /// the code.
1235 pub email: Option<String>,
1236 pub kind: InviteKind,
1237 /// The workspace it joins, by slug.
1238 pub workspace: Option<String>,
1239 pub status: InviteStatus,
1240 pub charged_to: InviteCharge,
1241 /// Who made it, by username. Null when g1t staff did.
1242 pub invited_by: Option<String>,
1243 /// The account that used it, by username.
1244 pub redeemed_by: Option<String>,
1245 /// RFC 3339.
1246 pub created_at: String,
1247 /// RFC 3339.
1248 pub expires_at: String,
1249 /// RFC 3339.
1250 pub redeemed_at: Option<String>,
1251 /// RFC 3339.
1252 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)1253 /// The account a workspace invitation is for, by username: someone
1254 /// invited by username, or the account the invite made.
1255 #[serde(default)]
1256 pub invitee: Option<String>,
1257 /// The role `workspace` is joined with. Null when it names none.
1258 #[serde(default)]
1259 pub role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1260 /// The staff member who minted it. Only in staff views.
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub staff: Option<String>,
1263}
1264
1265/// How many invites someone may have out, and how many they have.
1266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1267pub struct Allowance {
1268 /// Null: no limit.
1269 pub limit: Option<u32>,
1270 /// Pending and used invites; revoked and expired ones are not counted.
1271 pub used: u32,
1272 /// Null: no limit.
1273 pub remaining: Option<u32>,
1274}
1275
1276impl Allowance {
1277 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1278 Allowance {
1279 limit,
1280 used,
1281 remaining: limit.map(|limit| limit.saturating_sub(used)),
1282 }
1283 }
1284
1285 pub fn exhausted(&self) -> bool {
1286 self.remaining == Some(0)
1287 }
1288}
1289
1290/// A workspace's shared invites, for one of its owners.
1291#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1292pub struct WorkspaceAllowance {
1293 pub slug: String,
1294 pub allowance: Allowance,
1295}
1296
1297/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1298/// and what they have left. Returns `InvitesOverview`.
1299#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1300pub struct InvitesOverview {
1301 pub mode: RegistrationMode,
1302 pub allowance: Allowance,
1303 /// Workspaces the person owns that staff granted invites to.
1304 pub workspaces: Vec<WorkspaceAllowance>,
1305 pub invites: Vec<Invite>,
1306}
1307
1308/// `create_invite`: a person makes an invite, optionally for one email
1309/// address. People only; never an agent or a workspace's token, and not
1310/// before their email is confirmed. Uses one of the person's invites, or,
1311/// with `workspace`, one of the invites staff granted that workspace (its
1312/// owners only). Emails the address when one is given. Returns
1313/// `Outcome<Invite>`, with the code.
1314///
1315/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1316/// invite; a workspace's owners may revoke one made for the workspace.
1317/// The invite comes back to whoever it was charged to. Returns
1318/// `Outcome<Invite>`.
1319#[derive(Debug, Serialize, Deserialize)]
1320pub struct CreateInviteArgs {
1321 pub user: User,
1322 #[serde(default)]
1323 pub email: Option<String>,
1324 /// Use this workspace's granted invites, by slug.
1325 #[serde(default)]
1326 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)1327 /// The workspace the new account is invited to, by slug: one the
1328 /// person owns that can add members (not on the free plan). Once the
1329 /// account is confirmed it gets a workspace invitation to accept, as a
1330 /// member, and no workspace of its own is made for it.
1331 #[serde(default)]
1332 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)1333 /// The role `join` invites them with; member when absent. Ignored
1334 /// without `join`.
1335 #[serde(default)]
1336 pub join_role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1337 /// Where the request came in, for the audit log; g1t.sh when absent.
1338 #[serde(default)]
1339 pub surface: Option<crate::audit::Surface>,
1340}
1341
1342/// `check_invite`: what an invite code is for, before using it. Returns
1343/// `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 quotas1344/// expired gets the same answer, so codes cannot be probed. With
1345/// `any_status`, a real code that can no longer be used is described
1346/// instead (its `status` says why), so the page can say whom to ask for a
1347/// 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 look1348#[derive(Debug, Serialize, Deserialize)]
1349pub struct InviteCodeArgs {
1350 pub code: String,
1351 /// Who is asking, such as the visitor's IP address, for rate limits.
1352 #[serde(default)]
1353 pub client: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1354 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1355 #[serde(default)]
1356 pub viewer: Option<User>,
1357 #[serde(default)]
1358 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 confirm1359 /// The `proof` from the invite email's link, if the page was opened
1360 /// from it: sets `InvitePreview::email_proven`.
1361 #[serde(default)]
1362 pub email_proof: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1363}
1364
1365/// Someone shown on an invite.
1366#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1367pub struct InviteFrom {
1368 pub username: String,
1369 pub name: Option<String>,
1370 pub avatar: Option<String>,
1371}
1372
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1373/// A repository an invite code was sent with: using the code accepts the
1374/// invitation to collaborate on it.
1375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1376pub struct InviteRepository {
1377 /// `workspace/repo`.
1378 pub name: String,
1379 /// The role it gives, such as `write`.
1380 pub role: String,
1381}
1382
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1383/// What a valid invite code is for.
1384#[derive(Clone, Debug, Serialize, Deserialize)]
1385#[serde(rename_all = "camelCase")]
1386pub struct InvitePreview {
1387 pub kind: InviteKind,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1388 /// Pending, unless `any_status` asked about a code that is spent.
1389 pub status: InviteStatus,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1390 /// Null when g1t staff sent it.
1391 pub invited_by: Option<InviteFrom>,
1392 pub workspace: Option<ProfileWorkspace>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1393 /// The repository it accepts an invitation to, if it was sent with one.
1394 pub repository: Option<InviteRepository>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1395 /// The address it is for, partly hidden, such as `a•••@example.com`.
1396 pub email: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1397 /// The address in full, while it is pending: whoever holds the code
1398 /// was sent it there. Fills in and locks the sign-up form.
1399 pub address: Option<String>,
1400 /// Whether the address it is for has a g1t account already, so the
1401 /// page asks them to sign in rather than sign up.
1402 pub has_account: bool,
1403 /// With a viewer: whether the invite is theirs (it is for one of their
1404 /// confirmed addresses, or they used it). Null without a viewer or,
1405 /// for a pending invite, when it is for anyone with the code.
1406 pub for_viewer: Option<bool>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1407 /// RFC 3339.
1408 pub expires_at: String,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1409 /// For a shared invite link ([`SharedInvite`]): the group it was made
1410 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1411 /// it. Null for a one-person invite.
1412 #[serde(default)]
1413 pub shared_label: Option<String>,
1414 /// For a shared invite link limited to some email domains: those
1415 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1416 #[serde(default)]
1417 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 confirm1418 /// Whether `email_proof` was this pending invite's own, from the email
1419 /// it was sent in: the account made with it starts with `address`
1420 /// confirmed. False without a proof, with a wrong one, and for an
1421 /// invite bound to no address.
1422 #[serde(default)]
1423 pub email_proven: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1424}
1425
1426/// `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 quotas1427/// their confirmed address, and joins the workspace, or an invite sent with
1428/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1429/// workspace's slug, or `workspace/repo`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1430#[derive(Debug, Serialize, Deserialize)]
1431pub struct AcceptInviteArgs {
1432 pub user: User,
1433 pub code: String,
1434}
1435
1436/// `invite_member`: an owner invites an email address into a workspace.
1437/// It always makes an invite bound to that address and emails it, so the
1438/// answer never says whether the address has an account. Without one, the
1439/// invite registers and joins in one step, and uses one of the workspace's
1440/// granted invites or else one of the owner's own. With one, it costs
1441/// nothing. Returns `Outcome<Invite>`, with the code.
1442#[derive(Debug, Serialize, Deserialize)]
1443pub struct InviteMemberArgs {
1444 pub actor: User,
1445 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)1446 /// An email address. Give this or `username`.
1447 #[serde(default)]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1448 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)1449 /// A g1t username: that account gets a workspace invitation to accept
1450 /// or decline, in its inbox and by email. Nobody joins without saying
1451 /// yes.
1452 #[serde(default)]
1453 pub username: Option<String>,
1454 /// The role they join with; member when absent.
1455 #[serde(default)]
1456 pub role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1457 /// Where the request came in, for the audit log; g1t.sh when absent.
1458 #[serde(default)]
1459 pub surface: Option<crate::audit::Surface>,
1460}
1461
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)1462/// A workspace invitation waiting for its person's answer, as they see it.
1463/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1464/// newest first: pending ones only, never expired, revoked or answered.
1465#[derive(Clone, Debug, Serialize, Deserialize)]
1466#[serde(rename_all = "camelCase")]
1467pub struct WorkspaceInvitation {
1468 pub id: String,
1469 pub workspace: ProfileWorkspace,
1470 /// The role accepting joins with.
1471 pub role: crate::Role,
1472 /// Null when g1t staff sent it.
1473 pub invited_by: Option<InviteFrom>,
1474 /// RFC 3339.
1475 pub created_at: String,
1476 /// RFC 3339.
1477 pub expires_at: String,
1478}
1479
1480/// `accept_invitation`: the person it is for joins the workspace with the
1481/// role it names. Returns `Outcome<String>`, the workspace's slug.
1482///
1483/// `decline_invitation`: they say no; whoever sent it is told in their
1484/// inbox. Returns `Outcome<bool>`.
1485#[derive(Debug, Serialize, Deserialize)]
1486pub struct InvitationArgs {
1487 pub user: User,
1488 pub id: String,
1489 /// Where the request came in, for the audit log; g1t.sh when absent.
1490 #[serde(default)]
1491 pub surface: Option<crate::audit::Surface>,
1492}
1493
1494/// `find_people`: accounts whose username starts with `query`, or whose
1495/// name contains it, for picking someone to invite. Only what a profile
1496/// shows: a username, a name and an avatar, never an email address.
1497/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1498#[derive(Debug, Serialize, Deserialize)]
1499pub struct FindPeopleArgs {
1500 pub query: String,
1501 #[serde(default)]
1502 pub limit: Option<u32>,
1503}
1504
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1505/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1506/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1507///
1508/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1509#[derive(Debug, Serialize, Deserialize)]
1510pub struct WorkspaceInviteArgs {
1511 pub actor: User,
1512 pub slug: String,
1513 pub id: String,
1514}
1515
1516/// `request_access`: someone without an invite asks for one. Kept on the
1517/// waitlist, one entry per address. Answers the same way whether or not
1518/// the address is already on it. Returns `Outcome<bool>`.
1519#[derive(Debug, Default, Serialize, Deserialize)]
1520pub struct RequestAccessArgs {
1521 pub email: String,
1522 /// What they will build, if they said.
1523 #[serde(default)]
1524 pub about: String,
1525 /// Who is asking, such as the visitor's IP address, for rate limits.
1526 #[serde(default)]
1527 pub client: Option<String>,
1528}
1529
1530/// The most characters `RequestAccessArgs::about` keeps.
1531pub const MAX_WAITLIST_ABOUT: usize = 1000;
1532
1533// `registration` takes `{}` and returns `RegistrationMode`.
1534
1535// --- Invites, staff only ---
1536
1537#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1538#[serde(rename_all = "snake_case")]
1539pub enum WaitlistStatus {
1540 Waiting,
1541 Invited,
1542 Dismissed,
1543}
1544
1545impl WaitlistStatus {
1546 pub fn as_str(self) -> &'static str {
1547 match self {
1548 WaitlistStatus::Waiting => "waiting",
1549 WaitlistStatus::Invited => "invited",
1550 WaitlistStatus::Dismissed => "dismissed",
1551 }
1552 }
1553}
1554
1555/// Someone who asked for access.
1556#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1557#[serde(rename_all = "camelCase")]
1558pub struct WaitlistEntry {
1559 pub id: String,
1560 pub email: String,
1561 pub about: Option<String>,
1562 pub status: WaitlistStatus,
1563 pub invite_id: Option<String>,
1564 pub decided_by: Option<String>,
1565 /// RFC 3339.
1566 pub decided_at: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1567 /// What staff wrote when approving; it went in the invite email.
1568 #[serde(default)]
1569 pub note: Option<String>,
1570 /// The account made with the invite, once it was used.
1571 #[serde(default)]
1572 pub joined_as: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1573 /// When they first asked. RFC 3339.
1574 pub created_at: String,
1575 /// When they last asked. RFC 3339.
1576 pub updated_at: String,
1577}
1578
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1579/// `admin_waitlist`: the waitlist, newest first, at most
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1580/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1581///
1582/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1583/// still waiting, for sudo's navigation.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1584#[derive(Debug, Default, Serialize, Deserialize)]
1585pub struct AdminWaitlistArgs {
1586 /// Part of an email address or of what they said.
1587 #[serde(default)]
1588 pub query: Option<String>,
1589 /// Null: every status.
1590 #[serde(default)]
1591 pub status: Option<WaitlistStatus>,
1592}
1593
1594/// The most rows one staff listing of invites or the waitlist returns.
1595pub const ADMIN_INVITES_LIMIT: usize = 500;
1596
1597/// `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 quotas1598/// address, charged to nobody, and emails it, with `note` if given;
1599/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1600#[derive(Debug, Serialize, Deserialize)]
1601pub struct AdminDecideWaitlistArgs {
1602 pub id: String,
1603 pub approve: bool,
1604 /// The staff member, by email.
1605 pub staff: String,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1606 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1607 #[serde(default)]
1608 pub note: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1609}
1610
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1611/// The most characters an approval's note keeps.
1612pub const MAX_WAITLIST_NOTE: usize = 500;
1613
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1614/// `admin_invites`: every invite, newest first, at most
1615/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1616/// `query`, or whose email, inviter or redeemer contains it. Returns
1617/// `Vec<Invite>`.
1618#[derive(Debug, Default, Serialize, Deserialize)]
1619pub struct AdminInvitesArgs {
1620 #[serde(default)]
1621 pub query: Option<String>,
1622}
1623
1624/// `admin_revoke_invite`: revokes any pending invite. Returns
1625/// `Outcome<Invite>`.
1626#[derive(Debug, Serialize, Deserialize)]
1627pub struct AdminRevokeInviteArgs {
1628 pub id: String,
1629 pub staff: String,
1630}
1631
1632/// `admin_mint_invite`: staff make an invite that uses nobody's
1633/// allowance, optionally bound to (and emailed to) an address. Returns
1634/// `Outcome<Invite>`, with the code.
1635#[derive(Debug, Serialize, Deserialize)]
1636pub struct AdminMintInviteArgs {
1637 #[serde(default)]
1638 pub email: Option<String>,
1639 pub staff: String,
1640}
1641
1642/// Who staff grant invites to.
1643#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1644#[serde(rename_all = "snake_case")]
1645pub enum GrantTarget {
1646 User,
1647 Workspace,
1648}
1649
1650impl GrantTarget {
1651 pub fn as_str(self) -> &'static str {
1652 match self {
1653 GrantTarget::User => "user",
1654 GrantTarget::Workspace => "workspace",
1655 }
1656 }
1657}
1658
1659/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1660/// slug) `amount` more invites; a negative amount takes some back. Returns
1661/// `Outcome<Allowance>`: theirs afterwards.
1662#[derive(Debug, Serialize, Deserialize)]
1663pub struct AdminGrantInvitesArgs {
1664 pub target: GrantTarget,
1665 pub name: String,
1666 pub amount: i32,
1667 #[serde(default)]
1668 pub note: String,
1669 pub staff: String,
1670}
1671
1672/// The most invites one grant gives or takes back.
1673pub const MAX_INVITE_GRANT: i32 = 1000;
1674
1675/// Invites staff granted.
1676#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1677#[serde(rename_all = "camelCase")]
1678pub struct InviteGrant {
1679 pub amount: i32,
1680 pub note: Option<String>,
1681 pub granted_by: String,
1682 /// RFC 3339.
1683 pub created_at: String,
1684}
1685
1686/// Someone a person invited, and whom they invited in turn.
1687#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1688#[serde(rename_all = "camelCase")]
1689pub struct InviteTreeNode {
1690 pub username: String,
1691 /// When they used the invite. RFC 3339.
1692 pub joined_at: String,
1693 pub invited: Vec<InviteTreeNode>,
1694}
1695
1696/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1697/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1698///
1699/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1700/// invites, grants and invites. Returns `Option<InviteTree>` with
1701/// `username` the slug and no `invited_by`.
1702#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct InviteTree {
1705 pub username: String,
1706 /// Who invited them, then who invited that person, and so on. Empty
1707 /// for an account made without an invite.
1708 pub invited_by: Vec<String>,
1709 /// The staff member who minted their invite, when staff did.
1710 pub staff: Option<String>,
1711 pub allowance: Allowance,
1712 pub grants: Vec<InviteGrant>,
1713 /// Their invites, newest first.
1714 pub invites: Vec<Invite>,
1715 /// Whom they invited, three levels down.
1716 pub invited: Vec<InviteTreeNode>,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1717 /// The shared invite link the account was made with, if it was.
1718 #[serde(default)]
1719 pub shared: Option<SharedInviteSource>,
1720}
1721
1722// --- Shared invite links, staff only ---
1723//
1724// One link for a group (a conference's judges, a post, a community): up
1725// to `max_uses` new accounts, until it expires or staff revoke it,
1726// optionally only for addresses at some domains. Each use makes a new
1727// account, which then makes its own workspace; a shared link never joins
1728// anyone to an existing workspace, and uses nobody's allowance. Its code
1729// looks and is stored like any invite code (only a hash, and a sealed copy
1730// staff can copy again while it is live); the link is
1731// `https://g1t.sh/register?invite=<code>`. See
1732// services/identity/src/shared_invites.rs.
1733
1734/// How long a shared invite link works when staff give no date.
1735pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1736/// The furthest ahead a shared invite link's last day may be set.
1737pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1738/// The most accounts one shared invite link makes.
1739pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1740/// The most characters a shared invite link's label keeps.
1741pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1742/// The most email domains one shared invite link may be limited to.
1743pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1744
1745/// Where a shared invite link stands. Only a live one makes accounts.
1746#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1747#[serde(rename_all = "snake_case")]
1748pub enum SharedInviteStatus {
1749 Live,
1750 /// Every use is taken.
1751 UsedUp,
1752 Expired,
1753 Revoked,
1754}
1755
1756/// The shared invite link an account was made with.
1757#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1758pub struct SharedInviteSource {
1759 pub id: String,
1760 pub label: String,
1761}
1762
1763/// An account made with a shared invite link.
1764#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1765#[serde(rename_all = "camelCase")]
1766pub struct SharedInviteAccount {
1767 /// Null once the account is purged.
1768 pub username: Option<String>,
1769 /// When it was made with the link. RFC 3339.
1770 pub joined_at: String,
1771}
1772
1773/// One shared invite link, as staff see it.
1774#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1775#[serde(rename_all = "camelCase")]
1776pub struct SharedInvite {
1777 /// `sinv_…`.
1778 pub id: String,
1779 /// Whom it is for, such as `Cloudflare judges`.
1780 pub label: String,
1781 /// The code, while it is live (and IDENTITY_KEY is set).
1782 pub code: Option<String>,
1783 /// The code's first group, such as `g1t-k7m2`.
1784 pub hint: String,
1785 pub max_uses: u32,
1786 /// Accounts made with it so far.
1787 pub uses: u32,
1788 /// Only addresses at these domains may use it; empty for any.
1789 pub domains: Vec<String>,
1790 pub status: SharedInviteStatus,
1791 /// The staff member who made it, by email.
1792 pub staff: String,
1793 /// RFC 3339.
1794 pub created_at: String,
1795 /// RFC 3339.
1796 pub expires_at: String,
1797 pub revoked_at: Option<String>,
1798 pub revoked_by: Option<String>,
1799 /// The accounts made with it, oldest first.
1800 pub accounts: Vec<SharedInviteAccount>,
1801}
1802
1803/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1804/// at most 200, each with the accounts it made. Returns
1805/// `Vec<SharedInvite>`.
1806///
1807/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1808/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1809#[derive(Debug, Default, Serialize, Deserialize)]
1810pub struct AdminCreateSharedInviteArgs {
1811 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1812 pub label: String,
1813 /// 1 to [`MAX_SHARED_INVITE_USES`].
1814 pub max_uses: u32,
1815 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1816 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1817 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1818 #[serde(default)]
1819 pub expires_on: Option<String>,
1820 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1821 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1822 #[serde(default)]
1823 pub domains: Vec<String>,
1824 /// The staff member, by email.
1825 pub staff: String,
1826}
1827
1828/// `admin_revoke_shared_invite`: stops a shared invite link making any
1829/// more accounts. Those it made stay. Recorded in sudo's audit log.
1830/// Returns `Outcome<SharedInvite>`.
1831#[derive(Debug, Serialize, Deserialize)]
1832pub struct AdminRevokeSharedInviteArgs {
1833 pub id: String,
1834 pub staff: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1835}
1836
1837#[cfg(test)]
1838mod deletion_tests {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1839 use super::{WorkspaceDeletion, protected_names};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1840
1841 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1842 fn only_billing_or_protection_stands_in_the_way() {
1843 let clear = WorkspaceDeletion {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1844 repositories: 2,
1845 projects: 1,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1846 members: 3,
1847 ..WorkspaceDeletion::default()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1848 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1849 assert!(!clear.blocked());
1850 assert_eq!(clear.reason("acme"), None);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1851 let owing = WorkspaceDeletion {
1852 billing: Some("Pay first.".into()),
1853 ..WorkspaceDeletion::default()
1854 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1855 assert!(owing.blocked());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1856 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 member1857 let protected = WorkspaceDeletion {
1858 billing: Some("Pay first.".into()),
1859 protected: true,
1860 ..WorkspaceDeletion::default()
1861 };
1862 assert!(protected.blocked());
1863 assert_eq!(
1864 protected.reason("flagon-io").as_deref(),
1865 Some("flagon-io is protected and can never be deleted.")
1866 );
1867 }
1868
1869 #[test]
1870 fn flagon_is_protected_whatever_the_variable_says() {
1871 assert_eq!(protected_names(None), ["flagon-io"]);
1872 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1873 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1874 assert_eq!(
1875 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1876 ["flagon-io", "acme", "wsp_1"]
1877 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1878 }
1879}

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