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

API and MCP server, Rust identity service, registration, site redesign1//! 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,
RFC 3339 timestamps in identity and repos16 /// RFC 3339.
17 pub created_at: String,
API and MCP server, Rust identity service, registration, site redesign18}
19
Fine-grained personal tokens, workspace token rules and approvals in identity20#[derive(Clone, Debug, Default, Serialize, Deserialize)]
API and MCP server, Rust identity service, registration, site redesign21#[serde(rename_all = "camelCase")]
22pub struct AccessToken {
23 pub id: String,
24 pub name: String,
RFC 3339 timestamps in identity and repos25 /// RFC 3339.
26 pub created_at: String,
Agents as a team: lifecycle, merge queue, billing and a new shell27 /// RFC 3339, to within a few minutes. Null until it is first used.
28 pub last_used_at: Option<String>,
29 /// For a workspace's token, the username of the member who made it.
30 /// Null once that account is gone, and on personal tokens.
31 pub created_by: Option<String>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step32 /// Its scopes, as `resource:level`. Null: full access.
33 #[serde(default)]
34 pub scopes: Option<Vec<String>>,
35 /// Made before tokens had scopes: full access until someone narrows it.
36 #[serde(default)]
37 pub legacy: bool,
38 /// RFC 3339. Null: it does not expire.
39 #[serde(default)]
40 pub expires_at: Option<String>,
Fine-grained personal tokens, workspace token rules and approvals in identity41 /// Classic, fine-grained, or a workspace's own.
42 #[serde(default)]
43 pub kind: crate::tokens::TokenKind,
44 /// What it is for, as its owner wrote it.
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub description: Option<String>,
47 /// A fine-grained token's resource owner, repositories, permissions and
48 /// status.
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub fine_grained: Option<crate::tokens::FineGrainedDetails>,
51 /// A workspace's own token an owner gave Admin when making it.
52 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
53 pub admin: bool,
API and MCP server, Rust identity service, registration, site redesign54}
55
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look56/// `sign_in`: verifies a username, or any confirmed email address of the
57/// account, and its password, for website sign-in. Wrong passwords are
58/// counted against the account and `client`, and past a limit nothing is
59/// checked for a while (see identity's `throttle.rs`).
API and MCP server, Rust identity service, registration, site redesign60/// Returns `Outcome<SignedIn>`.
61#[derive(Debug, Serialize, Deserialize)]
62pub struct SignInArgs {
63 pub username: String,
64 pub password: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look65 /// Who is asking, such as the visitor's IP address, for rate limits.
66 #[serde(default)]
67 pub client: Option<String>,
API and MCP server, Rust identity service, registration, site redesign68}
69
70#[derive(Debug, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct SignedIn {
73 pub user: User,
74 pub session_token: String,
75}
76
77/// `sign_out` and `user_for_session`.
78#[derive(Debug, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct SessionArgs {
81 pub session_token: String,
82}
83
84/// `user_for_git_credentials`: the account password or an access token.
85#[derive(Debug, Serialize, Deserialize)]
86pub struct GitCredentialsArgs {
87 pub username: String,
88 pub secret: String,
89}
90
91/// `user_for_access_token`.
92#[derive(Debug, Serialize, Deserialize)]
93pub struct TokenArgs {
94 pub token: String,
95}
96
97/// `user_for_ssh_key`.
98#[derive(Debug, Serialize, Deserialize)]
99pub struct FingerprintArgs {
100 pub fingerprint: String,
101}
102
103/// `user_by_username`.
104#[derive(Debug, Serialize, Deserialize)]
105pub struct UsernameArgs {
106 pub username: String,
107}
108
What happened across an outcome, as a feed beside its graph109/// `usernames`: the names behind account and workspace ids, as events and
110/// other records store them. Returns a map from id to name; ids it does
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97111/// not know are left out. Also `accounts`: the accounts behind user ids,
112/// each with its username and avatar (`HashMap<String, accounts::EmailOwner>`).
What happened across an outcome, as a feed beside its graph113#[derive(Debug, Serialize, Deserialize)]
114pub struct UsernamesArgs {
115 pub ids: Vec<String>,
116}
117
API and MCP server, Rust identity service, registration, site redesign118/// `list_ssh_keys` and `list_access_tokens`.
119#[derive(Debug, Serialize, Deserialize)]
120pub struct UserArgs {
121 pub user: User,
122}
123
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge124/// `ssh_key_owners`: services only. The account (user id) that registered
125/// each key, by fingerprint (`SHA256:…`, as `ssh-keygen -lf` prints it),
126/// for verifying commits signed with SSH keys. Returns a map of the
127/// fingerprints found to user ids.
128#[derive(Debug, Serialize, Deserialize)]
129pub struct SshKeyOwnersArgs {
130 pub fingerprints: Vec<String>,
131}
132
API and MCP server, Rust identity service, registration, site redesign133/// `add_ssh_key`: `public_key` is one line in OpenSSH format.
134/// Returns `Outcome<SshKey>`.
135#[derive(Debug, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct AddSshKeyArgs {
138 pub user: User,
139 pub title: String,
140 pub public_key: String,
141}
142
143/// `remove_ssh_key` and `remove_access_token`.
144#[derive(Debug, Serialize, Deserialize)]
145pub struct RemoveArgs {
146 pub user: User,
147 pub id: String,
148}
149
Agents as a team: lifecycle, merge queue, billing and a new shell150/// `create_access_token`: a token that acts as `user`. For a workspace
151/// acting through a token of its own, the new token belongs to that
152/// workspace too.
API and MCP server, Rust identity service, registration, site redesign153#[derive(Debug, Serialize, Deserialize)]
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)154#[serde(rename_all = "camelCase")]
API and MCP server, Rust identity service, registration, site redesign155pub struct CreateAccessTokenArgs {
156 pub user: User,
157 pub name: String,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)158 /// 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 step159 /// left out of the user's token list, unless `listed`. Used for hosted
160 /// attempts.
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)161 #[serde(default)]
162 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 step163 /// Its scopes, as `resource:level`; unknown names are left out. Null:
164 /// full access.
165 #[serde(default)]
166 pub scopes: Option<Vec<String>>,
167 /// Listed with the person's tokens although it expires: one they made
168 /// themselves, with an expiry.
169 #[serde(default)]
170 pub listed: bool,
171}
172
Merge branch 'worktree-agent-a3abfcce648e87dca'173/// `create_job_token`: a workflow job's `G1T_TOKEN`. It acts as the
174/// repository's workspace, reaches that repository only, holds `scopes`
175/// (from the job's `permissions`), and is never listed. The actions service
176/// revokes it when the job ends (`revoke_job_tokens`); `ttl_seconds` is a
177/// backstop. Returns `CreatedAccessToken`.
178#[derive(Debug, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct CreateJobTokenArgs {
181 /// The workspace the repository belongs to, as its own principal.
182 pub workspace: User,
183 pub repo: crate::repos::RepoPath,
184 pub run_id: String,
185 pub job_id: String,
186 /// What the token is listed as in logs: `G1T_TOKEN for acme/web run 4`.
187 pub name: String,
188 pub ttl_seconds: u64,
189 /// As `resource:level`; unknown names are left out.
190 pub scopes: Vec<String>,
191 /// Whether it may open and approve pull requests (`JobToken::pull_requests`).
192 #[serde(default)]
193 pub pull_requests: bool,
194}
195
196/// `revoke_job_tokens`: ends a workflow job's tokens at once, when the job
197/// finishes or is cancelled. Only job tokens are touched. Returns `bool`.
198#[derive(Debug, Default, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct RevokeJobTokensArgs {
201 pub job_id: String,
202}
203
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step204/// `update_access_token`: changes what one of a person's tokens may do.
205/// The token itself is unchanged. Returns `Outcome<AccessToken>`.
206#[derive(Debug, Serialize, Deserialize)]
207pub struct UpdateAccessTokenArgs {
208 pub user: User,
209 pub id: String,
210 /// Null: full access.
211 #[serde(default)]
212 pub scopes: Option<Vec<String>>,
API and MCP server, Rust identity service, registration, site redesign213}
214
215/// The plaintext token is returned once and never stored.
216#[derive(Debug, Serialize, Deserialize)]
217pub struct CreatedAccessToken {
218 pub token: String,
219 pub info: AccessToken,
220}
221
222/// `register`: creates an account and signs it in.
223/// Returns `Outcome<SignedIn>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look224///
225/// While registration is invite-only (`REGISTRATION_MODE=invite`), every
226/// new account needs `invite_code`: an unused, unexpired invite, and, when
227/// the invite names an email, that address. See [`CreateInviteArgs`].
API and MCP server, Rust identity service, registration, site redesign228#[derive(Debug, Serialize, Deserialize)]
229pub struct RegisterArgs {
230 pub username: String,
231 pub email: String,
232 pub password: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look233 /// An invite code such as `g1t-k7m2-q9xd-4hpw-…`. Ignored while
234 /// registration is open.
235 #[serde(default)]
236 pub invite_code: Option<String>,
237 /// Who is asking, such as the visitor's IP address, for rate limits.
238 #[serde(default)]
239 pub client: Option<String>,
API and MCP server, Rust identity service, registration, site redesign240}
Email verification, password reset, and Git for AI scale positioning241
242/// `verify_email`: the token from the emailed link. Returns `Outcome<User>`.
243#[derive(Debug, Serialize, Deserialize)]
244pub struct EmailTokenArgs {
245 pub token: String,
246}
247
248/// `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 look249/// out which addresses have accounts. Any confirmed address of an account
250/// works: the link goes to the address given, and the primary (and the
251/// backup) are told a reset was asked for. A few requests an hour per
252/// address and per `client`; past that, nothing is sent.
Email verification, password reset, and Git for AI scale positioning253#[derive(Debug, Serialize, Deserialize)]
254pub struct EmailArgs {
255 pub email: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look256 /// Who is asking, such as the visitor's IP address, for rate limits.
257 #[serde(default)]
258 pub client: Option<String>,
Email verification, password reset, and Git for AI scale positioning259}
260
261/// `reset_password`: sets a new password and ends every session.
262/// Returns `Outcome<User>`.
263#[derive(Debug, Serialize, Deserialize)]
264pub struct ResetPasswordArgs {
265 pub token: String,
266 pub password: String,
267}
Device sign-in replaces registering and minting tokens over the API268
269/// `device_start`: begins a device sign-in. Returns `DeviceStart`.
270#[derive(Debug, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct DeviceStartArgs {
273 /// What is asking, shown to the person approving, e.g. "Claude Code".
274 pub client_name: String,
275}
276
277#[derive(Debug, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct DeviceStart {
280 /// Secret held by the tool and exchanged for a token once approved.
281 pub device_code: String,
282 /// Short code shown to the person, e.g. `WDJB-MJHT`.
283 pub user_code: String,
284 /// Seconds until both codes stop working.
285 pub expires_in: u32,
286 /// Seconds the tool should wait between polls.
287 pub interval: u32,
288}
289
290/// `device_lookup`: what a user code is asking for, or null if it is not
291/// valid. Returns `Option<DeviceRequest>`.
292#[derive(Debug, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub struct DeviceLookupArgs {
295 pub user_code: String,
296}
297
298#[derive(Debug, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct DeviceRequest {
301 pub user_code: String,
302 pub client_name: String,
303}
304
305/// `device_resolve`: the signed-in person approves or denies a request.
306/// Returns `Outcome<bool>`.
307#[derive(Debug, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct DeviceResolveArgs {
310 pub user_code: String,
311 pub user: User,
312 pub approve: bool,
313}
314
315/// `device_claim`: the tool asks whether its request was approved.
316#[derive(Debug, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct DeviceClaimArgs {
319 pub device_code: String,
320}
321
322/// The answer to a `device_claim`.
323#[derive(Debug, Serialize, Deserialize)]
324#[serde(tag = "status", rename_all = "snake_case")]
325pub enum DeviceClaim {
326 /// Nobody has approved or denied it yet; ask again after the interval.
327 Pending,
328 Denied,
329 /// The code was never issued, has expired, or was already used.
330 Expired,
331 /// The access token, returned once.
332 Approved {
333 token: String,
334 user: User,
335 },
336}
Workspaces own repositories337
338/// A workspace: the owner of repositories, and the first segment of their
339/// URLs. A person's own space and a team's are the same thing.
340#[derive(Clone, Debug, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct Workspace {
343 pub id: String,
344 pub slug: String,
345 pub name: String,
Agents as a team: lifecycle, merge queue, billing and a new shell346 /// One line saying what the workspace is for.
347 pub description: Option<String>,
Workspaces own repositories348 /// RFC 3339.
349 pub created_at: String,
350 pub member_count: u32,
Workspace names and icons, and a component kit for every control351 /// The workspace's uploaded icon: the SHA-256 of its bytes, served at
352 /// `/avatars/<avatar>`. Null means the generated letter avatar.
353 #[serde(default)]
354 pub avatar: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look355 /// What every member gets on each of its repositories; owners have
356 /// Admin. See [`crate::access`].
357 #[serde(default)]
358 pub base_permission: crate::access::BasePermission,
Merge branch 'worktree-agent-ad7c6d88d93adc817'359 /// Who may create its teams. See [`crate::teams::TeamCreation`].
360 #[serde(default)]
361 pub team_creation: crate::teams::TeamCreation,
Workspaces own repositories362}
363
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct Member {
366 pub username: String,
367 pub role: crate::Role,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look368 /// Their display name, when they set one.
369 #[serde(default)]
370 pub name: Option<String>,
371 /// Their uploaded avatar: the SHA-256 of its bytes, served at
372 /// `/avatars/<avatar>`. None means the generated letter avatar.
373 #[serde(default)]
374 pub avatar: Option<String>,
Workspaces own repositories375}
376
Merge branch 'worktree-agent-a2013627e5ea4ab13'377/// Where a workspace keeps its repositories' git data: anywhere g1t
378/// stores it (the default), or in the EU only. It applies to repositories
379/// made after it is set; the repos service reads it when it places a new
380/// one (`storage_options` says whether the EU can be chosen).
381#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(rename_all = "lowercase")]
383pub enum DataResidency {
384 #[default]
385 Anywhere,
386 Eu,
387}
388
389impl DataResidency {
390 pub fn as_str(self) -> &'static str {
391 match self {
392 DataResidency::Anywhere => "anywhere",
393 DataResidency::Eu => "eu",
394 }
395 }
396
397 pub fn parse(text: &str) -> Option<Self> {
398 match text.trim().to_ascii_lowercase().as_str() {
399 "anywhere" => Some(DataResidency::Anywhere),
400 "eu" => Some(DataResidency::Eu),
401 _ => None,
402 }
403 }
404}
405
406/// `workspace_residency` takes [`SlugArgs`] and returns
407/// `Option<DataResidency>` (null when there is no such workspace).
408/// `set_workspace_residency`: owners only. Returns `Outcome<DataResidency>`.
409#[derive(Debug, Serialize, Deserialize)]
410pub struct SetResidencyArgs {
411 pub actor: User,
412 pub slug: String,
413 pub residency: DataResidency,
414}
415
Workspaces own repositories416/// `create_workspace`. Returns `Outcome<Workspace>`.
417#[derive(Debug, Serialize, Deserialize)]
418pub struct CreateWorkspaceArgs {
419 pub user: User,
420 pub slug: String,
421 #[serde(default)]
422 pub name: String,
423}
424
425/// `get_workspace`: public details, or null. Returns `Option<Workspace>`.
426#[derive(Debug, Serialize, Deserialize)]
427pub struct SlugArgs {
428 pub slug: String,
429}
430
431/// `list_members`: members only. Returns `Outcome<Vec<Member>>`.
432#[derive(Debug, Serialize, Deserialize)]
433pub struct ListMembersArgs {
434 pub slug: String,
435 pub viewer: crate::Viewer,
436}
437
438/// `add_member` and `remove_member`: owners only.
439/// Each returns `Outcome<bool>`.
440#[derive(Debug, Serialize, Deserialize)]
441pub struct MemberArgs {
442 pub actor: User,
443 pub slug: String,
444 pub username: String,
445}
OAuth 2.1 sign-in for MCP clients and other applications446
Agents as a team: lifecycle, merge queue, billing and a new shell447/// `update_workspace`: owners only. An empty name falls back to the slug;
448/// an empty description clears it. Returns `Outcome<Workspace>`.
449#[derive(Debug, Serialize, Deserialize)]
450pub struct UpdateWorkspaceArgs {
451 pub actor: User,
452 pub slug: String,
453 pub name: String,
454 pub description: String,
455}
456
Agents and memory, checks and conflicts, profiles, slug renames, custom domains457/// `rename_workspace`: owners only. Changes the workspace's slug, the first
458/// segment of its URLs, to `new_slug`; the display name is untouched. The
459/// old slug redirects to the new one, and is held for this workspace, for
460/// [`SLUG_HOLD_DAYS`]. Publishes `workspace.renamed`. Returns
461/// `Outcome<Workspace>`.
462///
463/// `check_workspace_rename` takes the same arguments and answers whether
464/// the rename would be allowed, changing nothing. Returns `Outcome<bool>`.
465#[derive(Debug, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct RenameWorkspaceArgs {
468 pub actor: User,
469 pub slug: String,
470 pub new_slug: String,
471}
472
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look473/// `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 member474/// the workspace's slug, typed out. Refused for a protected workspace
475/// ([`protected_names`]), whoever asks, and while billing cannot settle it
476/// (`close_workspace`). Everything in it goes with it at once: nobody can
477/// reach it, its tokens stop working, its pages are not found, and its
478/// repositories, projects and apps are deleted with it. It is kept for
479/// [`WORKSPACE_RESTORE_DAYS`] so g1t's staff can restore it, then purged:
480/// its memberships, access tokens and old-slug redirects go, and billing's
481/// ledger and the audit log keep its history. The slug is never given to
482/// another workspace; the person whose username it is may make a workspace
483/// of that name again once it is purged. Publishes `workspace.deleting`,
484/// and `workspace.deleted` at the purge. Returns `Outcome<bool>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look485///
486/// `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 member487/// ignored) and says what would go and whether anything stands in the way,
488/// changing nothing. Returns `Outcome<WorkspaceDeletion>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look489#[derive(Debug, Serialize, Deserialize)]
490pub struct DeleteWorkspaceArgs {
491 pub actor: User,
492 pub slug: String,
493 #[serde(default)]
494 pub confirm: String,
495 /// Where the request came in, for the audit log; g1t.sh when absent.
496 #[serde(default)]
497 pub surface: Option<crate::audit::Surface>,
498}
499
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member500/// What deleting a workspace takes with it, and what stands in the way.
501/// 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 look502#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
503pub struct WorkspaceDeletion {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member504 /// Its live repositories, which are deleted with it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look505 pub repositories: u32,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member506 /// Its projects, hidden with it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look507 pub projects: u32,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member508 #[serde(default)]
509 pub members: u32,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look510 /// Why billing cannot close the workspace yet, in words for its owner.
511 pub billing: Option<String>,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member512 /// It can never be deleted, by anyone ([`protected_names`]).
513 #[serde(default)]
514 pub protected: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look515}
516
517impl WorkspaceDeletion {
518 pub fn blocked(&self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member519 self.protected || self.billing.is_some()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look520 }
521
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member522 /// 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 look523 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 member524 if self.protected {
525 return Some(protected_refusal(slug));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look526 }
527 self.billing.clone()
528 }
529}
530
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member531/// How long a deleted workspace is kept, for staff to restore, before it is
532/// purged.
533pub const WORKSPACE_RESTORE_DAYS: u64 = 30;
534
535/// Workspaces nobody can delete, whatever identity's `PROTECTED_WORKSPACES`
536/// says: Flagon's, which runs g1t.
537pub const ALWAYS_PROTECTED: &[&str] = &["flagon-io"];
538
539/// The protected workspaces: `configured` (comma-separated slugs or
540/// workspace ids, as identity's `PROTECTED_WORKSPACES` holds them), and
541/// [`ALWAYS_PROTECTED`] whatever it says, so an empty or missing variable
542/// still protects them. Lowercased, without duplicates.
543pub fn protected_names(configured: Option<&str>) -> Vec<String> {
544 let mut names: Vec<String> = Vec::new();
545 let given = configured.unwrap_or_default().split(',');
546 for name in ALWAYS_PROTECTED.iter().copied().chain(given) {
547 let name = name.trim().to_lowercase();
548 if !name.is_empty() && !names.contains(&name) {
549 names.push(name);
550 }
551 }
552 names
553}
554
555/// Why a protected workspace is not deleted, purged or acted on.
556pub fn protected_refusal(slug: &str) -> String {
557 format!("{slug} is protected and can never be deleted.")
558}
559
560/// `admin_deleted_workspaces` takes no arguments (`{}`) and returns
561/// `Vec<DeletedWorkspace>`, newest first. Staff only.
562///
563/// A workspace an owner deleted, kept until `purge_after` for staff to
564/// restore.
565#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct DeletedWorkspace {
568 pub workspace_id: String,
569 pub slug: String,
570 pub name: String,
571 /// RFC 3339.
572 pub deleted_at: String,
573 /// The username of the owner who deleted it.
574 pub deleted_by: String,
575 /// RFC 3339: when it is purged unless restored first.
576 pub purge_after: String,
577 /// What went with it, counted when it was deleted.
578 pub went: WorkspaceDeletion,
579 /// Whether staff can still restore it.
580 pub restorable: bool,
581}
582
583/// `admin_restore_workspace` and `admin_purge_workspace`: staff restore a
584/// deleted workspace within [`WORKSPACE_RESTORE_DAYS`], or purge it now.
585/// `staff` is who, for the audit logs. Purging needs `confirm`, the slug
586/// typed out, and is refused for a protected workspace. Restoring publishes
587/// `workspace.restored`; purging, `workspace.deleted`. Both return
588/// `Outcome<bool>`.
589#[derive(Debug, Serialize, Deserialize)]
590#[serde(rename_all = "camelCase")]
591pub struct AdminDeletedWorkspaceArgs {
592 pub workspace_id: String,
593 pub staff: String,
594 #[serde(default)]
595 pub confirm: String,
596}
597
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look598/// `transfer_repo_scopes`: a repository moved from `from` to `to`; the
599/// tokens of agents at work on it are kept pointing at it. For repos'
600/// `transfer`. Returns `bool`.
601#[derive(Debug, Serialize, Deserialize)]
602pub struct TransferRepoScopesArgs {
603 pub from: crate::repos::RepoPath,
604 pub to: crate::repos::RepoPath,
605}
606
Agents and memory, checks and conflicts, profiles, slug renames, custom domains607/// How long a workspace's old slug keeps redirecting to it, and stays
608/// reserved for it, after a rename.
609pub const SLUG_HOLD_DAYS: u64 = 90;
610
611/// How long a workspace must wait between renames.
612pub const RENAME_COOLDOWN_HOURS: u64 = 24;
613
614// `resolve_slug` takes `SlugArgs` and returns `Option<String>`: the
615// workspace's current slug when `slug` is one it was renamed from within
616// the last `SLUG_HOLD_DAYS`, and null otherwise (including for a slug that
Merge branch 'worktree-agent-a8385d293d42c913a'617// is in use), or the workspace's slug when `slug` is one of its aliases.
618
619// `resolve_alias` takes `SlugArgs` and returns `Option<String>`: the slug
620// now of the workspace `slug` is an alias of, and null when it is none.
621// Aliases are set by g1t's staff only: `g1t` is Flagon, Inc.'s `flagon-io`.
622// An alias follows its workspace through renames.
623
624/// `admin_aliases` takes no arguments (`{}`) and returns
625/// `Vec<WorkspaceAlias>`, by alias. Staff only.
626///
627/// A name staff point at a workspace, so that its addresses (pages, git,
628/// the API, packages) lead to the workspace under its own name.
629#[derive(Clone, Debug, Serialize, Deserialize)]
630#[serde(rename_all = "camelCase")]
631pub struct WorkspaceAlias {
632 pub alias: String,
633 pub workspace_id: String,
634 /// The workspace's slug and name now.
635 pub workspace: String,
636 pub workspace_name: String,
637 /// Why it exists, as staff wrote it.
638 pub note: String,
639 /// The staff member who set it, or `migration`.
640 pub created_by: String,
641 /// RFC 3339.
642 pub created_at: String,
643}
644
645/// `admin_set_alias`: points `alias` at the workspace whose slug is
646/// `workspace`. The alias must have a namespace's shape, must not be one of
647/// the site's routes, and must not be anyone's username, a workspace's slug
648/// (deleted, or held after a rename) or another alias. `note` is required:
649/// it is the reason, kept with the alias and in sudo's audit log. Staff
650/// only. Returns `Outcome<WorkspaceAlias>`.
651#[derive(Debug, Serialize, Deserialize)]
652#[serde(rename_all = "camelCase")]
653pub struct AdminSetAliasArgs {
654 pub alias: String,
655 pub workspace: String,
656 pub note: String,
657 pub staff: String,
658}
659
660/// `admin_remove_alias`: the alias stops leading anywhere, and the name is
661/// nobody's again unless it is reserved. `reason` goes in sudo's audit log.
662/// Staff only. Returns `Outcome<bool>`.
663#[derive(Debug, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub struct AdminRemoveAliasArgs {
666 pub alias: String,
667 pub reason: String,
668 pub staff: String,
669}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains670
Workspace names and icons, and a component kit for every control671/// `set_workspace_avatar`: owners only. `image` is the file's bytes in
672/// base64: PNG, JPEG, WebP or GIF, at most `MAX_AVATAR_BYTES`. Null removes
673/// the icon. Returns `Outcome<Workspace>`.
674#[derive(Debug, Serialize, Deserialize)]
675pub struct SetWorkspaceAvatarArgs {
676 pub actor: User,
677 pub slug: String,
678 pub image: Option<String>,
679}
680
681/// `set_user_avatar`: a person's own avatar, as `SetWorkspaceAvatarArgs`.
682/// Returns `Outcome<Option<String>>`: the new avatar, or null once removed.
683#[derive(Debug, Serialize, Deserialize)]
684pub struct SetUserAvatarArgs {
685 pub user: User,
686 pub image: Option<String>,
687}
688
689/// The largest avatar that can be uploaded, in bytes.
690pub const MAX_AVATAR_BYTES: usize = 1024 * 1024;
691
Agents as a team: lifecycle, merge queue, billing and a new shell692/// `list_workspace_tokens`: members only. Returns
693/// `Outcome<Vec<AccessToken>>`.
694#[derive(Debug, Serialize, Deserialize)]
695pub struct WorkspaceTokensArgs {
696 pub slug: String,
697 pub viewer: crate::Viewer,
698}
699
700/// `create_workspace_token`: owners only. The token belongs to the
701/// workspace, acts as it, and keeps working when the member who made it
702/// leaves. Returns `Outcome<CreatedAccessToken>`.
703#[derive(Debug, Serialize, Deserialize)]
704pub struct CreateWorkspaceTokenArgs {
705 pub actor: User,
706 pub slug: String,
707 pub name: String,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step708 /// Its scopes; null for full access.
709 #[serde(default)]
710 pub scopes: Option<Vec<String>>,
711 /// When set, the token stops working after this many seconds. It is
712 /// listed with the workspace's tokens either way. Null: no expiry.
713 #[serde(default)]
714 pub ttl_seconds: Option<u64>,
Fine-grained personal tokens, workspace token rules and approvals in identity715 /// Admin on the workspace's repositories, rather than Write: given by
716 /// the owner on purpose, when making it.
717 #[serde(default)]
718 pub admin: bool,
Agents as a team: lifecycle, merge queue, billing and a new shell719}
720
721/// `remove_workspace_token`: owners only. Returns `Outcome<bool>`.
722#[derive(Debug, Serialize, Deserialize)]
723pub struct RemoveWorkspaceTokenArgs {
724 pub actor: User,
725 pub slug: String,
726 pub id: String,
727}
728
OAuth 2.1 sign-in for MCP clients and other applications729/// `oauth_authorize`: the signed-in person approved an application. The
730/// caller has checked the client and that it may be redirected to
731/// `redirect_uri`. Returns `OAuthCode`.
732#[derive(Debug, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct OAuthAuthorizeArgs {
735 pub user: User,
736 pub client_id: String,
737 /// Shown wherever the application's access is listed.
738 pub client_name: String,
739 pub redirect_uri: String,
740 /// PKCE challenge, method S256.
741 pub code_challenge: String,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step742 /// What the person granted, as `resource:level`. Null: full access.
743 #[serde(default)]
744 pub scopes: Option<Vec<String>>,
OAuth 2.1 sign-in for MCP clients and other applications745}
746
747#[derive(Debug, Serialize, Deserialize)]
748pub struct OAuthCode {
749 pub code: String,
750}
751
752/// `oauth_exchange`: redeems an authorization code.
753/// Returns `Outcome<OAuthTokens>`.
754#[derive(Debug, Serialize, Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct OAuthExchangeArgs {
757 pub code: String,
758 pub code_verifier: String,
759 pub client_id: String,
760 pub redirect_uri: String,
761}
762
763/// `oauth_refresh`: trades a refresh token for new tokens.
764/// Returns `Outcome<OAuthTokens>`.
765#[derive(Debug, Serialize, Deserialize)]
766#[serde(rename_all = "camelCase")]
767pub struct OAuthRefreshArgs {
768 pub refresh_token: String,
769 pub client_id: String,
770}
771
772#[derive(Debug, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct OAuthTokens {
775 pub access_token: String,
776 /// Works once; using it returns the next one.
777 pub refresh_token: String,
778 /// Seconds until the access token stops working.
779 pub expires_in: u64,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step780 /// The scopes granted, space-separated, or `*` for full access.
781 #[serde(default)]
782 pub scope: Option<String>,
OAuth 2.1 sign-in for MCP clients and other applications783}
784
785/// An application a person has signed in to. Listed by `list_oauth_grants`
786/// and ended by `revoke_oauth_grant`.
787#[derive(Debug, Serialize, Deserialize)]
788#[serde(rename_all = "camelCase")]
789pub struct OAuthGrant {
790 pub id: String,
791 pub client_name: String,
792 /// RFC 3339.
793 pub created_at: String,
794 /// RFC 3339.
795 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 step796 /// What the person granted. Null: full access.
797 #[serde(default)]
798 pub scopes: Option<Vec<String>>,
799 /// Signed in before applications were given scopes: full access until
800 /// someone narrows it.
801 #[serde(default)]
802 pub legacy: bool,
803}
804
805/// `update_oauth_grant`: changes what an application the person signed in
806/// to may do, at once and when it refreshes. Returns `Outcome<OAuthGrant>`.
807#[derive(Debug, Serialize, Deserialize)]
808pub struct UpdateOAuthGrantArgs {
809 pub user: User,
810 pub id: String,
811 #[serde(default)]
812 pub scopes: Option<Vec<String>>,
OAuth 2.1 sign-in for MCP clients and other applications813}
Agents as a team: lifecycle, merge queue, billing and a new shell814
815
816/// 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 API817#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Agents as a team: lifecycle, merge queue, billing and a new shell818pub struct AgentScope {
819 pub repo: crate::repos::RepoPath,
820 /// API and MCP operation names, such as `create_issue`.
821 pub operations: Vec<String>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API822 /// Set on a run credential: the run it belongs to, and what it may do
823 /// with git. See [`crate::credentials`].
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub run: Option<crate::credentials::RunBinding>,
Agents as a team: lifecycle, merge queue, billing and a new shell826}
827
828/// `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-agent829/// behalf. It acts as `g1t`, a member of the repository's workspace,
Agents as a team: lifecycle, merge queue, billing and a new shell830/// and only for the operations in `scope`. Returns `CreatedAccessToken`.
831#[derive(Debug, Serialize, Deserialize)]
832#[serde(rename_all = "camelCase")]
833pub struct CreateAgentTokenArgs {
834 /// The person the agent works for; the token is recorded as theirs.
835 pub on_behalf_of: User,
836 pub scope: AgentScope,
837 pub ttl_seconds: u64,
838}
839
840// `agent_scope` takes `TokenArgs` and returns `Option<AgentScope>`: what an
841// agent's token may do, or null for any other token.
842
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent843/// The id g1t's agent acts under. Only ever stored, never shown: it keeps
844/// the agent's work apart from g1t's own ([`crate::system::ID`]) where
845/// that matters, such as whether its approval counts.
Agents as a team: lifecycle, merge queue, billing and a new shell846pub 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-agent847/// The name g1t's agent is shown by: g1t's own, [`crate::system::USERNAME`].
848/// Everything it does, people see g1t do.
849pub const AGENT_NAME: &str = crate::system::USERNAME;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace850
851// --- Staff ---------------------------------------------------------------
852//
853// Staff-only methods, for sudo.g1t.sh. They take no viewer and check no
854// membership: only sudo calls them, over its service binding, after it has
855// verified a Cloudflare Access sign-in and its staff list. Nothing a
856// customer can reach should ever forward to them.
857
858/// `notify_owners`: emails a short notice, with one link, to each owner of
859/// a workspace with a confirmed address. Called by other services (billing
860/// warns owners near their usage limit), never on a person's behalf.
861/// Returns how many were sent.
862#[derive(Clone, Debug, Serialize, Deserialize)]
863pub struct NotifyOwnersArgs {
864 pub workspace: String,
865 pub subject: String,
866 /// One or two sentences: what happened and what it means.
867 pub intro: String,
868 /// The button's words, such as `Open billing`.
869 pub action: String,
870 /// Where the button goes; must be on g1t.sh.
871 pub link: String,
872 /// Small print: why they got it.
873 pub footer: String,
874}
875
876/// `admin_workspaces`: every workspace, newest first, at most
877/// [`ADMIN_WORKSPACES_LIMIT`], optionally only those whose slug, name or
878/// an owner's username or email contains `query`. Returns
879/// `Vec<AdminWorkspace>`. Staff only.
880#[derive(Debug, Default, Serialize, Deserialize)]
881pub struct AdminWorkspacesArgs {
882 #[serde(default)]
883 pub query: Option<String>,
884}
885
886/// The most workspaces one `admin_workspaces` call returns.
887pub const ADMIN_WORKSPACES_LIMIT: usize = 500;
888
889/// An owner of a workspace, as staff see them.
890#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
891pub struct AdminOwner {
892 pub username: String,
893 pub email: Option<String>,
894}
895
896/// A workspace as staff see it: who owns it and how many belong to it.
897#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
898#[serde(rename_all = "camelCase")]
899pub struct AdminWorkspace {
900 pub slug: String,
901 pub name: String,
902 /// RFC 3339.
903 pub created_at: String,
904 pub owners: Vec<AdminOwner>,
905 pub member_count: u32,
906}
907
908/// `admin_workspace`: one workspace with every member, or null. Takes
909/// `SlugArgs`; returns `Option<AdminWorkspaceDetail>`. Staff only.
910#[derive(Clone, Debug, Serialize, Deserialize)]
911#[serde(rename_all = "camelCase")]
912pub struct AdminWorkspaceDetail {
913 pub slug: String,
914 pub name: String,
915 pub description: Option<String>,
916 /// RFC 3339.
917 pub created_at: String,
918 /// Owners first, then by username.
919 pub members: Vec<AdminMember>,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member920 /// It can never be deleted ([`protected_names`]).
921 #[serde(default)]
922 pub protected: bool,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace923}
924
925/// A member of a workspace, as staff see them.
926#[derive(Clone, Debug, Serialize, Deserialize)]
927pub struct AdminMember {
928 pub username: String,
929 pub email: Option<String>,
930 pub role: crate::Role,
931 /// When they joined the workspace. RFC 3339.
932 pub joined: String,
933}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains934
935// --- Profiles ------------------------------------------------------------
936//
937// A person's public page at `g1t.sh/u/<username>`. Everything in a
938// `Profile` is shown to anyone, signed in or not; an email address never is.
939
940/// The most characters each profile field takes.
941pub const MAX_PROFILE_NAME: usize = 80;
942pub const MAX_PROFILE_BIO: usize = 160;
943pub const MAX_PROFILE_LOCATION: usize = 80;
944pub const MAX_PROFILE_WEBSITE: usize = 200;
945pub const MAX_PROFILE_PRONOUNS: usize = 40;
946
947/// What anyone may see about a person.
948#[derive(Clone, Debug, Default, Serialize, Deserialize)]
949#[serde(rename_all = "camelCase")]
950pub struct Profile {
951 pub username: String,
952 /// The name they go by, if they gave one.
953 pub name: Option<String>,
954 /// One or two lines about them, at most [`MAX_PROFILE_BIO`] characters.
955 pub bio: Option<String>,
956 pub location: Option<String>,
957 /// An `https://` address.
958 pub website: Option<String>,
959 pub pronouns: Option<String>,
960 /// The uploaded avatar's hash, served at `/avatars/<avatar>`.
961 pub avatar: Option<String>,
962 /// When the account was made. RFC 3339.
963 pub created_at: String,
964}
965
966// `profile` takes `UsernameArgs` and returns `Option<Profile>`: null for
967// an account that does not exist.
968
969/// `update_profile`: a person changes their own profile. Every field is
970/// replaced; an empty one is cleared. Returns `Outcome<Profile>`.
971#[derive(Debug, Default, Serialize, Deserialize)]
972#[serde(rename_all = "camelCase")]
973pub struct UpdateProfileArgs {
974 pub actor: User,
975 #[serde(default)]
976 pub name: String,
977 #[serde(default)]
978 pub bio: String,
979 #[serde(default)]
980 pub location: String,
981 #[serde(default)]
982 pub website: String,
983 #[serde(default)]
984 pub pronouns: String,
985}
986
987/// `profile_workspaces`: the workspaces shown on a person's profile, as
988/// `viewer` may see them. A membership is shown only when it is no secret
989/// from the viewer: a workspace the viewer belongs to as well, or one of
990/// `public`, the workspaces the caller found the person has made a public
991/// project in (whose page shows that already). Returns
992/// `Vec<ProfileWorkspace>`; empty for an account that does not exist.
993#[derive(Debug, Serialize, Deserialize)]
994pub struct ProfileWorkspacesArgs {
995 pub username: String,
996 pub viewer: crate::Viewer,
997 #[serde(default)]
998 pub public: Vec<String>,
999}
1000
1001/// A workspace on a person's profile.
1002#[derive(Clone, Debug, Serialize, Deserialize)]
1003pub struct ProfileWorkspace {
1004 pub slug: String,
1005 pub name: String,
1006 pub avatar: Option<String>,
1007}
Search across all of g1t, Explore, and a command palette1008
1009/// `directory`: every account or every workspace, as their public pages
1010/// show them, a page at a time in name order. For services that index
1011/// them, such as search; nothing private is in it. Returns
1012/// `DirectoryPage`.
1013#[derive(Debug, Default, Serialize, Deserialize)]
1014pub struct DirectoryArgs {
1015 /// `user` or `workspace`.
1016 pub kind: String,
1017 /// Names after this one.
1018 #[serde(default)]
1019 pub after: Option<String>,
1020 pub limit: u32,
1021}
1022
1023/// One account or workspace in the directory.
1024#[derive(Clone, Debug, Serialize, Deserialize)]
1025#[serde(rename_all = "camelCase")]
1026pub struct DirectoryEntry {
1027 /// The account's or workspace's id.
1028 pub id: String,
1029 /// A username or a workspace's slug.
1030 pub slug: String,
1031 /// A person's display name or a workspace's name.
1032 pub name: Option<String>,
1033 /// A person's bio or a workspace's description.
1034 pub bio: Option<String>,
1035 pub avatar: Option<String>,
1036 /// RFC 3339.
1037 pub created_at: String,
1038}
1039
1040#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1041pub struct DirectoryPage {
1042 pub entries: Vec<DirectoryEntry>,
1043 /// Where the next page starts; null on the last.
1044 pub next: Option<String>,
1045}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1046
1047// --- Invites ---------------------------------------------------------------
1048//
1049// While registration is invite-only, every new account (with a password or
1050// through GitHub) needs an invite code. Each person may have
1051// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1052// to a workspace, whose owners share them. Inviting an email with no
1053// account into a workspace makes an invite bound to that address, which
1054// registers and joins in one step. See services/identity/src/invites.rs.
1055
1056/// Whether anyone may make an account, or only someone with an invite. Set
1057/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1058/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1059#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1060#[serde(rename_all = "snake_case")]
1061pub enum RegistrationMode {
1062 #[default]
1063 Invite,
1064 Open,
1065}
1066
1067impl RegistrationMode {
1068 pub fn parse(text: Option<&str>) -> RegistrationMode {
1069 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1070 Some("open") => RegistrationMode::Open,
1071 _ => RegistrationMode::Invite,
1072 }
1073 }
1074}
1075
1076/// How many invites a person may have out at once, unless identity's
1077/// `INVITES_PER_USER` var says otherwise.
1078pub const INVITES_PER_USER: u32 = 5;
1079
1080/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1081/// otherwise.
1082pub const INVITE_TTL_DAYS: u64 = 30;
1083
1084/// Where an invite stands. Only a pending invite can be used or revoked.
1085/// An expired or revoked invite that was never used gives its inviter the
1086/// invite back.
1087#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1088#[serde(rename_all = "snake_case")]
1089pub enum InviteStatus {
1090 Pending,
1091 Redeemed,
1092 Expired,
1093 Revoked,
1094}
1095
1096/// What using an invite does.
1097#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1098#[serde(rename_all = "snake_case")]
1099pub enum InviteKind {
1100 /// Makes a new account, and joins `workspace` when one is set.
1101 Account,
1102 /// An existing account joins `workspace`. Never makes an account.
1103 Workspace,
1104}
1105
1106/// Whose allowance an invite uses.
1107#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1108#[serde(rename_all = "snake_case")]
1109pub enum InviteCharge {
1110 /// Its inviter's own.
1111 User,
1112 /// The workspace's, granted by staff and shared by its owners.
1113 Workspace,
1114 /// Nobody's: staff minted it, or it invites an existing account.
1115 None,
1116}
1117
1118/// One invite, as the person who made it sees it.
1119#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121pub struct Invite {
1122 pub id: String,
1123 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1124 /// is made, and afterwards to whoever made it while it is pending.
1125 /// Null otherwise.
1126 pub code: Option<String>,
1127 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1128 pub hint: String,
1129 /// Only an account with this address can use it. Null: anyone with
1130 /// the code.
1131 pub email: Option<String>,
1132 pub kind: InviteKind,
1133 /// The workspace it joins, by slug.
1134 pub workspace: Option<String>,
1135 pub status: InviteStatus,
1136 pub charged_to: InviteCharge,
1137 /// Who made it, by username. Null when g1t staff did.
1138 pub invited_by: Option<String>,
1139 /// The account that used it, by username.
1140 pub redeemed_by: Option<String>,
1141 /// RFC 3339.
1142 pub created_at: String,
1143 /// RFC 3339.
1144 pub expires_at: String,
1145 /// RFC 3339.
1146 pub redeemed_at: Option<String>,
1147 /// RFC 3339.
1148 pub revoked_at: Option<String>,
1149 /// The staff member who minted it. Only in staff views.
1150 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 pub staff: Option<String>,
1152}
1153
1154/// How many invites someone may have out, and how many they have.
1155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct Allowance {
1157 /// Null: no limit.
1158 pub limit: Option<u32>,
1159 /// Pending and used invites; revoked and expired ones are not counted.
1160 pub used: u32,
1161 /// Null: no limit.
1162 pub remaining: Option<u32>,
1163}
1164
1165impl Allowance {
1166 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1167 Allowance {
1168 limit,
1169 used,
1170 remaining: limit.map(|limit| limit.saturating_sub(used)),
1171 }
1172 }
1173
1174 pub fn exhausted(&self) -> bool {
1175 self.remaining == Some(0)
1176 }
1177}
1178
1179/// A workspace's shared invites, for one of its owners.
1180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1181pub struct WorkspaceAllowance {
1182 pub slug: String,
1183 pub allowance: Allowance,
1184}
1185
1186/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1187/// and what they have left. Returns `InvitesOverview`.
1188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1189pub struct InvitesOverview {
1190 pub mode: RegistrationMode,
1191 pub allowance: Allowance,
1192 /// Workspaces the person owns that staff granted invites to.
1193 pub workspaces: Vec<WorkspaceAllowance>,
1194 pub invites: Vec<Invite>,
1195}
1196
1197/// `create_invite`: a person makes an invite, optionally for one email
1198/// address. People only; never an agent or a workspace's token, and not
1199/// before their email is confirmed. Uses one of the person's invites, or,
1200/// with `workspace`, one of the invites staff granted that workspace (its
1201/// owners only). Emails the address when one is given. Returns
1202/// `Outcome<Invite>`, with the code.
1203///
1204/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1205/// invite; a workspace's owners may revoke one made for the workspace.
1206/// The invite comes back to whoever it was charged to. Returns
1207/// `Outcome<Invite>`.
1208#[derive(Debug, Serialize, Deserialize)]
1209pub struct CreateInviteArgs {
1210 pub user: User,
1211 #[serde(default)]
1212 pub email: Option<String>,
1213 /// Use this workspace's granted invites, by slug.
1214 #[serde(default)]
1215 pub workspace: Option<String>,
1216 /// Where the request came in, for the audit log; g1t.sh when absent.
1217 #[serde(default)]
1218 pub surface: Option<crate::audit::Surface>,
1219}
1220
1221/// `check_invite`: what an invite code is for, before using it. Returns
1222/// `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 quotas1223/// expired gets the same answer, so codes cannot be probed. With
1224/// `any_status`, a real code that can no longer be used is described
1225/// instead (its `status` says why), so the page can say whom to ask for a
1226/// 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 look1227#[derive(Debug, Serialize, Deserialize)]
1228pub struct InviteCodeArgs {
1229 pub code: String,
1230 /// Who is asking, such as the visitor's IP address, for rate limits.
1231 #[serde(default)]
1232 pub client: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1233 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1234 #[serde(default)]
1235 pub viewer: Option<User>,
1236 #[serde(default)]
1237 pub any_status: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1238}
1239
1240/// Someone shown on an invite.
1241#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1242pub struct InviteFrom {
1243 pub username: String,
1244 pub name: Option<String>,
1245 pub avatar: Option<String>,
1246}
1247
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1248/// A repository an invite code was sent with: using the code accepts the
1249/// invitation to collaborate on it.
1250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1251pub struct InviteRepository {
1252 /// `workspace/repo`.
1253 pub name: String,
1254 /// The role it gives, such as `write`.
1255 pub role: String,
1256}
1257
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1258/// What a valid invite code is for.
1259#[derive(Clone, Debug, Serialize, Deserialize)]
1260#[serde(rename_all = "camelCase")]
1261pub struct InvitePreview {
1262 pub kind: InviteKind,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1263 /// Pending, unless `any_status` asked about a code that is spent.
1264 pub status: InviteStatus,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1265 /// Null when g1t staff sent it.
1266 pub invited_by: Option<InviteFrom>,
1267 pub workspace: Option<ProfileWorkspace>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1268 /// The repository it accepts an invitation to, if it was sent with one.
1269 pub repository: Option<InviteRepository>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1270 /// The address it is for, partly hidden, such as `a•••@example.com`.
1271 pub email: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1272 /// The address in full, while it is pending: whoever holds the code
1273 /// was sent it there. Fills in and locks the sign-up form.
1274 pub address: Option<String>,
1275 /// Whether the address it is for has a g1t account already, so the
1276 /// page asks them to sign in rather than sign up.
1277 pub has_account: bool,
1278 /// With a viewer: whether the invite is theirs (it is for one of their
1279 /// confirmed addresses, or they used it). Null without a viewer or,
1280 /// for a pending invite, when it is for anyone with the code.
1281 pub for_viewer: Option<bool>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1282 /// RFC 3339.
1283 pub expires_at: String,
1284}
1285
1286/// `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 quotas1287/// their confirmed address, and joins the workspace, or an invite sent with
1288/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1289/// workspace's slug, or `workspace/repo`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1290#[derive(Debug, Serialize, Deserialize)]
1291pub struct AcceptInviteArgs {
1292 pub user: User,
1293 pub code: String,
1294}
1295
1296/// `invite_member`: an owner invites an email address into a workspace.
1297/// It always makes an invite bound to that address and emails it, so the
1298/// answer never says whether the address has an account. Without one, the
1299/// invite registers and joins in one step, and uses one of the workspace's
1300/// granted invites or else one of the owner's own. With one, it costs
1301/// nothing. Returns `Outcome<Invite>`, with the code.
1302#[derive(Debug, Serialize, Deserialize)]
1303pub struct InviteMemberArgs {
1304 pub actor: User,
1305 pub slug: String,
1306 pub email: String,
1307 /// Where the request came in, for the audit log; g1t.sh when absent.
1308 #[serde(default)]
1309 pub surface: Option<crate::audit::Surface>,
1310}
1311
1312/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1313/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1314///
1315/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1316#[derive(Debug, Serialize, Deserialize)]
1317pub struct WorkspaceInviteArgs {
1318 pub actor: User,
1319 pub slug: String,
1320 pub id: String,
1321}
1322
1323/// `request_access`: someone without an invite asks for one. Kept on the
1324/// waitlist, one entry per address. Answers the same way whether or not
1325/// the address is already on it. Returns `Outcome<bool>`.
1326#[derive(Debug, Default, Serialize, Deserialize)]
1327pub struct RequestAccessArgs {
1328 pub email: String,
1329 /// What they will build, if they said.
1330 #[serde(default)]
1331 pub about: String,
1332 /// Who is asking, such as the visitor's IP address, for rate limits.
1333 #[serde(default)]
1334 pub client: Option<String>,
1335}
1336
1337/// The most characters `RequestAccessArgs::about` keeps.
1338pub const MAX_WAITLIST_ABOUT: usize = 1000;
1339
1340// `registration` takes `{}` and returns `RegistrationMode`.
1341
1342// --- Invites, staff only ---
1343
1344#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1345#[serde(rename_all = "snake_case")]
1346pub enum WaitlistStatus {
1347 Waiting,
1348 Invited,
1349 Dismissed,
1350}
1351
1352impl WaitlistStatus {
1353 pub fn as_str(self) -> &'static str {
1354 match self {
1355 WaitlistStatus::Waiting => "waiting",
1356 WaitlistStatus::Invited => "invited",
1357 WaitlistStatus::Dismissed => "dismissed",
1358 }
1359 }
1360}
1361
1362/// Someone who asked for access.
1363#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1364#[serde(rename_all = "camelCase")]
1365pub struct WaitlistEntry {
1366 pub id: String,
1367 pub email: String,
1368 pub about: Option<String>,
1369 pub status: WaitlistStatus,
1370 pub invite_id: Option<String>,
1371 pub decided_by: Option<String>,
1372 /// RFC 3339.
1373 pub decided_at: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1374 /// What staff wrote when approving; it went in the invite email.
1375 #[serde(default)]
1376 pub note: Option<String>,
1377 /// The account made with the invite, once it was used.
1378 #[serde(default)]
1379 pub joined_as: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1380 /// When they first asked. RFC 3339.
1381 pub created_at: String,
1382 /// When they last asked. RFC 3339.
1383 pub updated_at: String,
1384}
1385
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1386/// `admin_waitlist`: the waitlist, newest first, at most
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1387/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1388///
1389/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1390/// still waiting, for sudo's navigation.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1391#[derive(Debug, Default, Serialize, Deserialize)]
1392pub struct AdminWaitlistArgs {
1393 /// Part of an email address or of what they said.
1394 #[serde(default)]
1395 pub query: Option<String>,
1396 /// Null: every status.
1397 #[serde(default)]
1398 pub status: Option<WaitlistStatus>,
1399}
1400
1401/// The most rows one staff listing of invites or the waitlist returns.
1402pub const ADMIN_INVITES_LIMIT: usize = 500;
1403
1404/// `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 quotas1405/// address, charged to nobody, and emails it, with `note` if given;
1406/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1407#[derive(Debug, Serialize, Deserialize)]
1408pub struct AdminDecideWaitlistArgs {
1409 pub id: String,
1410 pub approve: bool,
1411 /// The staff member, by email.
1412 pub staff: String,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1413 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1414 #[serde(default)]
1415 pub note: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1416}
1417
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1418/// The most characters an approval's note keeps.
1419pub const MAX_WAITLIST_NOTE: usize = 500;
1420
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1421/// `admin_invites`: every invite, newest first, at most
1422/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1423/// `query`, or whose email, inviter or redeemer contains it. Returns
1424/// `Vec<Invite>`.
1425#[derive(Debug, Default, Serialize, Deserialize)]
1426pub struct AdminInvitesArgs {
1427 #[serde(default)]
1428 pub query: Option<String>,
1429}
1430
1431/// `admin_revoke_invite`: revokes any pending invite. Returns
1432/// `Outcome<Invite>`.
1433#[derive(Debug, Serialize, Deserialize)]
1434pub struct AdminRevokeInviteArgs {
1435 pub id: String,
1436 pub staff: String,
1437}
1438
1439/// `admin_mint_invite`: staff make an invite that uses nobody's
1440/// allowance, optionally bound to (and emailed to) an address. Returns
1441/// `Outcome<Invite>`, with the code.
1442#[derive(Debug, Serialize, Deserialize)]
1443pub struct AdminMintInviteArgs {
1444 #[serde(default)]
1445 pub email: Option<String>,
1446 pub staff: String,
1447}
1448
1449/// Who staff grant invites to.
1450#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1451#[serde(rename_all = "snake_case")]
1452pub enum GrantTarget {
1453 User,
1454 Workspace,
1455}
1456
1457impl GrantTarget {
1458 pub fn as_str(self) -> &'static str {
1459 match self {
1460 GrantTarget::User => "user",
1461 GrantTarget::Workspace => "workspace",
1462 }
1463 }
1464}
1465
1466/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1467/// slug) `amount` more invites; a negative amount takes some back. Returns
1468/// `Outcome<Allowance>`: theirs afterwards.
1469#[derive(Debug, Serialize, Deserialize)]
1470pub struct AdminGrantInvitesArgs {
1471 pub target: GrantTarget,
1472 pub name: String,
1473 pub amount: i32,
1474 #[serde(default)]
1475 pub note: String,
1476 pub staff: String,
1477}
1478
1479/// The most invites one grant gives or takes back.
1480pub const MAX_INVITE_GRANT: i32 = 1000;
1481
1482/// Invites staff granted.
1483#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1484#[serde(rename_all = "camelCase")]
1485pub struct InviteGrant {
1486 pub amount: i32,
1487 pub note: Option<String>,
1488 pub granted_by: String,
1489 /// RFC 3339.
1490 pub created_at: String,
1491}
1492
1493/// Someone a person invited, and whom they invited in turn.
1494#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1495#[serde(rename_all = "camelCase")]
1496pub struct InviteTreeNode {
1497 pub username: String,
1498 /// When they used the invite. RFC 3339.
1499 pub joined_at: String,
1500 pub invited: Vec<InviteTreeNode>,
1501}
1502
1503/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1504/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1505///
1506/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1507/// invites, grants and invites. Returns `Option<InviteTree>` with
1508/// `username` the slug and no `invited_by`.
1509#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1510#[serde(rename_all = "camelCase")]
1511pub struct InviteTree {
1512 pub username: String,
1513 /// Who invited them, then who invited that person, and so on. Empty
1514 /// for an account made without an invite.
1515 pub invited_by: Vec<String>,
1516 /// The staff member who minted their invite, when staff did.
1517 pub staff: Option<String>,
1518 pub allowance: Allowance,
1519 pub grants: Vec<InviteGrant>,
1520 /// Their invites, newest first.
1521 pub invites: Vec<Invite>,
1522 /// Whom they invited, three levels down.
1523 pub invited: Vec<InviteTreeNode>,
1524}
1525
1526#[cfg(test)]
1527mod deletion_tests {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1528 use super::{WorkspaceDeletion, protected_names};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1529
1530 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1531 fn only_billing_or_protection_stands_in_the_way() {
1532 let clear = WorkspaceDeletion {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1533 repositories: 2,
1534 projects: 1,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1535 members: 3,
1536 ..WorkspaceDeletion::default()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1537 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1538 assert!(!clear.blocked());
1539 assert_eq!(clear.reason("acme"), None);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1540 let owing = WorkspaceDeletion {
1541 billing: Some("Pay first.".into()),
1542 ..WorkspaceDeletion::default()
1543 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1544 assert!(owing.blocked());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1545 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 member1546 let protected = WorkspaceDeletion {
1547 billing: Some("Pay first.".into()),
1548 protected: true,
1549 ..WorkspaceDeletion::default()
1550 };
1551 assert!(protected.blocked());
1552 assert_eq!(
1553 protected.reason("flagon-io").as_deref(),
1554 Some("flagon-io is protected and can never be deleted.")
1555 );
1556 }
1557
1558 #[test]
1559 fn flagon_is_protected_whatever_the_variable_says() {
1560 assert_eq!(protected_names(None), ["flagon-io"]);
1561 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1562 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1563 assert_eq!(
1564 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1565 ["flagon-io", "acme", "wsp_1"]
1566 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1567 }
1568}

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