Skip to content
1,977 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 reference examples use projects1//! 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>,
API reference examples use projects22}
23
Fine-grained personal tokens, workspace token rules and approvals in identity24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
API reference examples use projects25#[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,
API reference examples use projects85}
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`).
API reference examples use projects91/// 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>,
API reference examples use projects99}
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.
API reference examples use projects107 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>,
API reference examples use projects113}
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`]).
API reference examples use projects136#[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>`).
API reference examples use projects151#[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
API reference examples use projects165/// `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
API reference examples use projects180/// `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.
API reference examples use projects208 #[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
API reference examples use projects251/// 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`].
API reference examples use projects264#[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>,
API reference examples use projects281}
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.
API reference examples use projects294#[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>,
API reference examples use projects300}
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,
API reference examples use projects412}
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>,
API reference examples use projects421 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>,
API reference examples use projects436}
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
API reference examples use projects477/// `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>>`.
API reference examples use projects494#[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>`.
API reference examples use projects506#[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>,
API reference examples use projects513}
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
API reference examples use projects761/// `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>>,
API reference examples use projects793}
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>,
API reference examples use projects831}
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>>,
API reference examples use projects861}
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)]
API reference examples use projects866pub 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>,
API reference examples use projects874}
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,
API reference examples use projects878/// 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.
API reference examples use projects894pub const AGENT_ID: &str = "usr_g1t_agent";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent895/// The name g1t's agent is shown by: g1t's own, [`crate::system::USERNAME`].
896/// Everything it does, people see g1t do.
897pub const AGENT_NAME: &str = crate::system::USERNAME;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace898
899// --- Staff ---------------------------------------------------------------
900//
901// Staff-only methods, for sudo.g1t.sh. They take no viewer and check no
902// membership: only sudo calls them, over its service binding, after it has
903// verified a Cloudflare Access sign-in and its staff list. Nothing a
904// customer can reach should ever forward to them.
905
906/// `notify_owners`: emails a short notice, with one link, to each owner of
907/// a workspace with a confirmed address. Called by other services (billing
908/// warns owners near their usage limit), never on a person's behalf.
909/// Returns how many were sent.
910#[derive(Clone, Debug, Serialize, Deserialize)]
911pub struct NotifyOwnersArgs {
912 pub workspace: String,
913 pub subject: String,
914 /// One or two sentences: what happened and what it means.
915 pub intro: String,
916 /// The button's words, such as `Open billing`.
917 pub action: String,
918 /// Where the button goes; must be on g1t.sh.
919 pub link: String,
920 /// Small print: why they got it.
921 pub footer: String,
922}
923
924/// `admin_workspaces`: every workspace, newest first, at most
925/// [`ADMIN_WORKSPACES_LIMIT`], optionally only those whose slug, name or
926/// an owner's username or email contains `query`. Returns
927/// `Vec<AdminWorkspace>`. Staff only.
928#[derive(Debug, Default, Serialize, Deserialize)]
929pub struct AdminWorkspacesArgs {
930 #[serde(default)]
931 pub query: Option<String>,
932}
933
934/// The most workspaces one `admin_workspaces` call returns.
935pub const ADMIN_WORKSPACES_LIMIT: usize = 500;
936
937/// An owner of a workspace, as staff see them.
938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
939pub struct AdminOwner {
940 pub username: String,
941 pub email: Option<String>,
942}
943
944/// A workspace as staff see it: who owns it and how many belong to it.
945#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
946#[serde(rename_all = "camelCase")]
947pub struct AdminWorkspace {
948 pub slug: String,
949 pub name: String,
950 /// RFC 3339.
951 pub created_at: String,
952 pub owners: Vec<AdminOwner>,
953 pub member_count: u32,
954}
955
956/// `admin_workspace`: one workspace with every member, or null. Takes
957/// `SlugArgs`; returns `Option<AdminWorkspaceDetail>`. Staff only.
958#[derive(Clone, Debug, Serialize, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct AdminWorkspaceDetail {
961 pub slug: String,
962 pub name: String,
963 pub description: Option<String>,
964 /// RFC 3339.
965 pub created_at: String,
966 /// Owners first, then by username.
967 pub members: Vec<AdminMember>,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member968 /// It can never be deleted ([`protected_names`]).
969 #[serde(default)]
970 pub protected: bool,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace971}
972
973/// A member of a workspace, as staff see them.
974#[derive(Clone, Debug, Serialize, Deserialize)]
975pub struct AdminMember {
976 pub username: String,
977 pub email: Option<String>,
978 pub role: crate::Role,
979 /// When they joined the workspace. RFC 3339.
980 pub joined: String,
981}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains982
983// --- Profiles ------------------------------------------------------------
984//
985// A person's public page at `g1t.sh/u/<username>`. Everything in a
986// `Profile` is shown to anyone, signed in or not; an email address never is.
987
988/// The most characters each profile field takes.
989pub const MAX_PROFILE_NAME: usize = 80;
990pub const MAX_PROFILE_BIO: usize = 160;
991pub const MAX_PROFILE_LOCATION: usize = 80;
992pub const MAX_PROFILE_WEBSITE: usize = 200;
993pub const MAX_PROFILE_PRONOUNS: usize = 40;
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)994pub const MAX_PROFILE_TIMEZONE: usize = 64;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains995
996/// What anyone may see about a person.
997#[derive(Clone, Debug, Default, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct Profile {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1000 /// Lowercased: what the profile is found and linked by.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1001 pub username: String,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1002 /// The username as its owner wrote it, when that differs: what the page shows.
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub display_username: Option<String>,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1005 /// The name they go by, if they gave one.
1006 pub name: Option<String>,
1007 /// One or two lines about them, at most [`MAX_PROFILE_BIO`] characters.
1008 pub bio: Option<String>,
1009 pub location: Option<String>,
1010 /// An `https://` address.
1011 pub website: Option<String>,
1012 pub pronouns: Option<String>,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)1013 /// The time zone they are in, an IANA name such as `America/Denver`.
1014 #[serde(default)]
1015 pub timezone: Option<String>,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1016 /// The uploaded avatar's hash, served at `/avatars/<avatar>`.
1017 pub avatar: Option<String>,
1018 /// When the account was made. RFC 3339.
1019 pub created_at: String,
1020}
1021
1022// `profile` takes `UsernameArgs` and returns `Option<Profile>`: null for
1023// an account that does not exist.
1024
1025/// `update_profile`: a person changes their own profile. Every field is
1026/// replaced; an empty one is cleared. Returns `Outcome<Profile>`.
1027#[derive(Debug, Default, Serialize, Deserialize)]
1028#[serde(rename_all = "camelCase")]
1029pub struct UpdateProfileArgs {
1030 pub actor: User,
1031 #[serde(default)]
1032 pub name: String,
1033 #[serde(default)]
1034 pub bio: String,
1035 #[serde(default)]
1036 pub location: String,
1037 #[serde(default)]
1038 pub website: String,
1039 #[serde(default)]
1040 pub pronouns: String,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)1041 /// An IANA time zone name, such as `America/Denver`.
1042 #[serde(default)]
1043 pub timezone: String,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1044}
1045
1046/// `profile_workspaces`: the workspaces shown on a person's profile, as
1047/// `viewer` may see them. A membership is shown only when it is no secret
1048/// from the viewer: a workspace the viewer belongs to as well, or one of
1049/// `public`, the workspaces the caller found the person has made a public
1050/// project in (whose page shows that already). Returns
1051/// `Vec<ProfileWorkspace>`; empty for an account that does not exist.
1052#[derive(Debug, Serialize, Deserialize)]
1053pub struct ProfileWorkspacesArgs {
1054 pub username: String,
1055 pub viewer: crate::Viewer,
1056 #[serde(default)]
1057 pub public: Vec<String>,
1058}
1059
1060/// A workspace on a person's profile.
1061#[derive(Clone, Debug, Serialize, Deserialize)]
1062pub struct ProfileWorkspace {
1063 pub slug: String,
1064 pub name: String,
1065 pub avatar: Option<String>,
1066}
Search across all of g1t, Explore, and a command palette1067
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.1068// --- Dock pins -------------------------------------------------------------
1069//
1070// The apps a person pins to their dock in a workspace, kept with their
1071// account so the dock follows them to every device. Each person's own:
1072// nobody else reads or sets them.
1073
1074/// The most apps a person pins in one workspace.
1075pub const MAX_DOCK_PINS: usize = 24;
1076/// The most characters an app key takes.
1077pub const MAX_DOCK_APP_KEY: usize = 32;
1078
1079/// `dock_pins`: the apps `user` pinned in the workspace `workspace` (a
1080/// slug), in the order they set. Returns `Option<Vec<String>>`: null when
1081/// they never saved any there, or are not one of its members.
1082#[derive(Debug, Default, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase")]
1084pub struct DockPinsArgs {
1085 pub user: User,
1086 pub workspace: String,
1087}
1088
1089/// `set_dock_pins`: replaces `user`'s pins in `workspace` with `apps`, in
1090/// that order. Each key is lowercase letters, digits and hyphens, at most
1091/// [`MAX_DOCK_APP_KEY`] characters; a repeat is dropped; at most
1092/// [`MAX_DOCK_PINS`]. Which keys name real apps is the web app's to say.
1093/// Returns `Outcome<Vec<String>>`: the pins as saved.
1094#[derive(Debug, Default, Serialize, Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct SetDockPinsArgs {
1097 pub user: User,
1098 pub workspace: String,
1099 #[serde(default)]
1100 pub apps: Vec<String>,
1101}
1102
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.1103// --- Sounds and desktop notifications --------------------------------------
1104//
1105// What a person hears when chat moves, kept with their account so a phone,
1106// a laptop and the desktop app all sound the same. Which sound plays when
1107// is the web app's rule (apps/web/app/lib/chat-sounds.ts); identity keeps
1108// the choices. Do not disturb is not here: it is the person's presence
1109// (`dnd_until`, services/notify), which also silences pop-ups and pushes.
1110// Mirrors `SoundSettings` in packages/contracts/src/identity.ts.
1111
1112/// The sets of sounds a person can choose from.
1113pub const SOUND_SETS: [&str; 2] = ["soft", "bright"];
1114/// The cues a person can turn on and off. `call` is reserved and has no setting.
1115pub const SOUND_CUES: [&str; 5] = ["message", "direct", "mention", "agent_done", "sent"];
1116/// Before anyone changes them: every cue but the tick as you send.
1117pub const SOUND_CUES_OFF_BY_DEFAULT: [&str; 1] = ["sent"];
1118pub const DEFAULT_SOUND_SET: &str = "soft";
1119pub const DEFAULT_SOUND_VOLUME: u8 = 60;
1120
1121/// A person's sound settings, as kept and as answered. Wire fields are
1122/// snake_case, as the site's JSON is.
1123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1124pub struct SoundSettings {
1125 /// Whether anything plays at all.
1126 pub sounds_enabled: bool,
1127 /// One of [`SOUND_SETS`].
1128 pub sound_set: String,
1129 /// 0 to 100.
1130 pub sound_volume: u8,
1131 /// Each cue on or off, by name; a cue left out is on, except `sent`.
1132 #[serde(default)]
1133 pub sound_cues: std::collections::BTreeMap<String, bool>,
1134 /// Whether an open tab shows a system notification for a message to
1135 /// you while its window is not in front.
1136 pub desktop_toasts: bool,
1137}
1138
1139impl Default for SoundSettings {
1140 fn default() -> Self {
1141 SoundSettings {
1142 sounds_enabled: true,
1143 sound_set: DEFAULT_SOUND_SET.to_owned(),
1144 sound_volume: DEFAULT_SOUND_VOLUME,
1145 sound_cues: SOUND_CUES
1146 .iter()
1147 .map(|cue| ((*cue).to_owned(), !SOUND_CUES_OFF_BY_DEFAULT.contains(cue)))
1148 .collect(),
1149 desktop_toasts: false,
1150 }
1151 }
1152}
1153
1154/// A change to them: only what is sent changes; `sound_cues` merges by cue.
1155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct SoundSettingsChange {
1157 #[serde(default)]
1158 pub sounds_enabled: Option<bool>,
1159 #[serde(default)]
1160 pub sound_set: Option<String>,
1161 #[serde(default)]
1162 pub sound_volume: Option<i64>,
1163 #[serde(default)]
1164 pub sound_cues: Option<std::collections::BTreeMap<String, bool>>,
1165 #[serde(default)]
1166 pub desktop_toasts: Option<bool>,
1167}
1168
1169/// `sound_settings`: `user`'s own, or the defaults when they never changed
1170/// them. Returns `SoundSettings`. People only.
1171#[derive(Debug, Default, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct SoundSettingsArgs {
1174 pub user: User,
1175}
1176
1177/// `set_sound_settings`: changes `user`'s own. Returns
1178/// `Outcome<SoundSettings>`: the settings as kept, or `invalid` for a set
1179/// or cue g1t does not have, or a volume outside 0 to 100.
1180#[derive(Debug, Default, Serialize, Deserialize)]
1181#[serde(rename_all = "camelCase")]
1182pub struct SetSoundSettingsArgs {
1183 pub user: User,
1184 #[serde(default)]
1185 pub change: SoundSettingsChange,
1186}
1187
Search across all of g1t, Explore, and a command palette1188/// `directory`: every account or every workspace, as their public pages
1189/// show them, a page at a time in name order. For services that index
1190/// them, such as search; nothing private is in it. Returns
1191/// `DirectoryPage`.
1192#[derive(Debug, Default, Serialize, Deserialize)]
1193pub struct DirectoryArgs {
1194 /// `user` or `workspace`.
1195 pub kind: String,
1196 /// Names after this one.
1197 #[serde(default)]
1198 pub after: Option<String>,
1199 pub limit: u32,
1200}
1201
1202/// One account or workspace in the directory.
1203#[derive(Clone, Debug, Serialize, Deserialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct DirectoryEntry {
1206 /// The account's or workspace's id.
1207 pub id: String,
1208 /// A username or a workspace's slug.
1209 pub slug: String,
1210 /// A person's display name or a workspace's name.
1211 pub name: Option<String>,
1212 /// A person's bio or a workspace's description.
1213 pub bio: Option<String>,
1214 pub avatar: Option<String>,
1215 /// RFC 3339.
1216 pub created_at: String,
1217}
1218
1219#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1220pub struct DirectoryPage {
1221 pub entries: Vec<DirectoryEntry>,
1222 /// Where the next page starts; null on the last.
1223 pub next: Option<String>,
1224}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1225
1226// --- Invites ---------------------------------------------------------------
1227//
1228// While registration is invite-only, every new account (with a password or
1229// through GitHub) needs an invite code. Each person may have
1230// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1231// to a workspace, whose owners share them. Inviting an email with no
1232// account into a workspace makes an invite bound to that address, which
1233// registers and joins in one step. See services/identity/src/invites.rs.
1234
1235/// Whether anyone may make an account, or only someone with an invite. Set
1236/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1237/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1239#[serde(rename_all = "snake_case")]
1240pub enum RegistrationMode {
1241 #[default]
1242 Invite,
1243 Open,
1244}
1245
1246impl RegistrationMode {
1247 pub fn parse(text: Option<&str>) -> RegistrationMode {
1248 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1249 Some("open") => RegistrationMode::Open,
1250 _ => RegistrationMode::Invite,
1251 }
1252 }
1253}
1254
1255/// How many invites a person may have out at once, unless identity's
1256/// `INVITES_PER_USER` var says otherwise.
1257pub const INVITES_PER_USER: u32 = 5;
1258
1259/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1260/// otherwise.
1261pub const INVITE_TTL_DAYS: u64 = 30;
1262
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1263/// Where an invite stands. Only a pending invite can be used. A pending
1264/// invite can be revoked, and so can one awaiting confirmation. An expired
1265/// or revoked invite that was never used gives its inviter the invite back.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1267#[serde(rename_all = "snake_case")]
1268pub enum InviteStatus {
1269 Pending,
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1270 /// Used to make an account that has not confirmed its email address
1271 /// yet. The code is spent; the workspace (or repository) it gives is
1272 /// joined when the address is confirmed, unless it is revoked first.
1273 AwaitingConfirmation,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1274 /// Used to make an account that has confirmed its address, for a
1275 /// workspace it has not yet joined or declined: the workspace
1276 /// invitation waits for the person's answer (`accept_invitation`).
1277 AwaitingAnswer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1278 Redeemed,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1279 /// Its person declined the workspace it invited them to.
1280 Declined,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1281 Expired,
1282 Revoked,
1283}
1284
1285/// What using an invite does.
1286#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1287#[serde(rename_all = "snake_case")]
1288pub enum InviteKind {
1289 /// Makes a new account, and joins `workspace` when one is set.
1290 Account,
1291 /// An existing account joins `workspace`. Never makes an account.
1292 Workspace,
1293}
1294
1295/// Whose allowance an invite uses.
1296#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1297#[serde(rename_all = "snake_case")]
1298pub enum InviteCharge {
1299 /// Its inviter's own.
1300 User,
1301 /// The workspace's, granted by staff and shared by its owners.
1302 Workspace,
1303 /// Nobody's: staff minted it, or it invites an existing account.
1304 None,
1305}
1306
1307/// One invite, as the person who made it sees it.
1308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1309#[serde(rename_all = "camelCase")]
1310pub struct Invite {
1311 pub id: String,
1312 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1313 /// is made, and afterwards to whoever made it while it is pending.
1314 /// Null otherwise.
1315 pub code: Option<String>,
1316 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1317 pub hint: String,
1318 /// Only an account with this address can use it. Null: anyone with
1319 /// the code.
1320 pub email: Option<String>,
1321 pub kind: InviteKind,
1322 /// The workspace it joins, by slug.
1323 pub workspace: Option<String>,
1324 pub status: InviteStatus,
1325 pub charged_to: InviteCharge,
1326 /// Who made it, by username. Null when g1t staff did.
1327 pub invited_by: Option<String>,
1328 /// The account that used it, by username.
1329 pub redeemed_by: Option<String>,
1330 /// RFC 3339.
1331 pub created_at: String,
1332 /// RFC 3339.
1333 pub expires_at: String,
1334 /// RFC 3339.
1335 pub redeemed_at: Option<String>,
1336 /// RFC 3339.
1337 pub revoked_at: Option<String>,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1338 /// The account a workspace invitation is for, by username: someone
1339 /// invited by username, or the account the invite made.
1340 #[serde(default)]
1341 pub invitee: Option<String>,
1342 /// The role `workspace` is joined with. Null when it names none.
1343 #[serde(default)]
1344 pub role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1345 /// The staff member who minted it. Only in staff views.
1346 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub staff: Option<String>,
1348}
1349
1350/// How many invites someone may have out, and how many they have.
1351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1352pub struct Allowance {
1353 /// Null: no limit.
1354 pub limit: Option<u32>,
1355 /// Pending and used invites; revoked and expired ones are not counted.
1356 pub used: u32,
1357 /// Null: no limit.
1358 pub remaining: Option<u32>,
1359}
1360
1361impl Allowance {
1362 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1363 Allowance {
1364 limit,
1365 used,
1366 remaining: limit.map(|limit| limit.saturating_sub(used)),
1367 }
1368 }
1369
1370 pub fn exhausted(&self) -> bool {
1371 self.remaining == Some(0)
1372 }
1373}
1374
1375/// A workspace's shared invites, for one of its owners.
1376#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1377pub struct WorkspaceAllowance {
1378 pub slug: String,
1379 pub allowance: Allowance,
1380}
1381
1382/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1383/// and what they have left. Returns `InvitesOverview`.
1384#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1385pub struct InvitesOverview {
1386 pub mode: RegistrationMode,
1387 pub allowance: Allowance,
1388 /// Workspaces the person owns that staff granted invites to.
1389 pub workspaces: Vec<WorkspaceAllowance>,
1390 pub invites: Vec<Invite>,
1391}
1392
1393/// `create_invite`: a person makes an invite, optionally for one email
1394/// address. People only; never an agent or a workspace's token, and not
1395/// before their email is confirmed. Uses one of the person's invites, or,
1396/// with `workspace`, one of the invites staff granted that workspace (its
1397/// owners only). Emails the address when one is given. Returns
1398/// `Outcome<Invite>`, with the code.
1399///
1400/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1401/// invite; a workspace's owners may revoke one made for the workspace.
1402/// The invite comes back to whoever it was charged to. Returns
1403/// `Outcome<Invite>`.
1404#[derive(Debug, Serialize, Deserialize)]
1405pub struct CreateInviteArgs {
1406 pub user: User,
1407 #[serde(default)]
1408 pub email: Option<String>,
1409 /// Use this workspace's granted invites, by slug.
1410 #[serde(default)]
1411 pub workspace: Option<String>,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1412 /// The workspace the new account is invited to, by slug: one the
1413 /// person owns that can add members (not on the free plan). Once the
1414 /// account is confirmed it gets a workspace invitation to accept, as a
1415 /// member, and no workspace of its own is made for it.
1416 #[serde(default)]
1417 pub join: Option<String>,
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)1418 /// The role `join` invites them with; member when absent. Ignored
1419 /// without `join`.
1420 #[serde(default)]
1421 pub join_role: Option<crate::Role>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1422 /// Where the request came in, for the audit log; g1t.sh when absent.
1423 #[serde(default)]
1424 pub surface: Option<crate::audit::Surface>,
1425}
1426
1427/// `check_invite`: what an invite code is for, before using it. Returns
1428/// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1429/// expired gets the same answer, so codes cannot be probed. With
1430/// `any_status`, a real code that can no longer be used is described
1431/// instead (its `status` says why), so the page can say whom to ask for a
1432/// new one; an unknown code still gets the one answer.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1433#[derive(Debug, Serialize, Deserialize)]
1434pub struct InviteCodeArgs {
1435 pub code: String,
1436 /// Who is asking, such as the visitor's IP address, for rate limits.
1437 #[serde(default)]
1438 pub client: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1439 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1440 #[serde(default)]
1441 pub viewer: Option<User>,
1442 #[serde(default)]
1443 pub any_status: bool,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm1444 /// The `proof` from the invite email's link, if the page was opened
1445 /// from it: sets `InvitePreview::email_proven`.
1446 #[serde(default)]
1447 pub email_proof: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1448}
1449
1450/// Someone shown on an invite.
1451#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1452pub struct InviteFrom {
1453 pub username: String,
1454 pub name: Option<String>,
1455 pub avatar: Option<String>,
1456}
1457
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1458/// A repository an invite code was sent with: using the code accepts the
1459/// invitation to collaborate on it.
1460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1461pub struct InviteRepository {
1462 /// `workspace/repo`.
1463 pub name: String,
1464 /// The role it gives, such as `write`.
1465 pub role: String,
1466}
1467
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1468/// What a valid invite code is for.
1469#[derive(Clone, Debug, Serialize, Deserialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct InvitePreview {
1472 pub kind: InviteKind,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1473 /// Pending, unless `any_status` asked about a code that is spent.
1474 pub status: InviteStatus,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1475 /// Null when g1t staff sent it.
1476 pub invited_by: Option<InviteFrom>,
1477 pub workspace: Option<ProfileWorkspace>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1478 /// The repository it accepts an invitation to, if it was sent with one.
1479 pub repository: Option<InviteRepository>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1480 /// The address it is for, partly hidden, such as `a•••@example.com`.
1481 pub email: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1482 /// The address in full, while it is pending: whoever holds the code
1483 /// was sent it there. Fills in and locks the sign-up form.
1484 pub address: Option<String>,
1485 /// Whether the address it is for has a g1t account already, so the
1486 /// page asks them to sign in rather than sign up.
1487 pub has_account: bool,
1488 /// With a viewer: whether the invite is theirs (it is for one of their
1489 /// confirmed addresses, or they used it). Null without a viewer or,
1490 /// for a pending invite, when it is for anyone with the code.
1491 pub for_viewer: Option<bool>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1492 /// RFC 3339.
1493 pub expires_at: String,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1494 /// For a shared invite link ([`SharedInvite`]): the group it was made
1495 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1496 /// it. Null for a one-person invite.
1497 #[serde(default)]
1498 pub shared_label: Option<String>,
1499 /// For a shared invite link limited to some email domains: those
1500 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1501 #[serde(default)]
1502 pub shared_domains: Vec<String>,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm1503 /// Whether `email_proof` was this pending invite's own, from the email
1504 /// it was sent in: the account made with it starts with `address`
1505 /// confirmed. False without a proof, with a wrong one, and for an
1506 /// invite bound to no address.
1507 #[serde(default)]
1508 pub email_proven: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1509}
1510
1511/// `accept_invite`: a signed-in person uses a workspace invite made for
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1512/// their confirmed address, and joins the workspace, or an invite sent with
1513/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1514/// workspace's slug, or `workspace/repo`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1515#[derive(Debug, Serialize, Deserialize)]
1516pub struct AcceptInviteArgs {
1517 pub user: User,
1518 pub code: String,
1519}
1520
1521/// `invite_member`: an owner invites an email address into a workspace.
1522/// It always makes an invite bound to that address and emails it, so the
1523/// answer never says whether the address has an account. Without one, the
1524/// invite registers and joins in one step, and uses one of the workspace's
1525/// granted invites or else one of the owner's own. With one, it costs
1526/// nothing. Returns `Outcome<Invite>`, with the code.
1527#[derive(Debug, Serialize, Deserialize)]
1528pub struct InviteMemberArgs {
1529 pub actor: User,
1530 pub slug: String,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1531 /// An email address. Give this or `username`.
1532 #[serde(default)]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1533 pub email: String,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)1534 /// A g1t username: that account gets a workspace invitation to accept
1535 /// or decline, in its inbox and by email. Nobody joins without saying
1536 /// yes.
1537 #[serde(default)]
1538 pub username: Option<String>,
1539 /// The role they join with; member when absent.
1540 #[serde(default)]
1541 pub role: Option<crate::Role>,
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.1542 /// A note from whoever invites, quoted in the invitation email. Up to
1543 /// [`MAX_INVITE_MESSAGE`] characters; longer is cut, blank is none.
1544 #[serde(default)]
1545 pub message: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1546 /// Where the request came in, for the audit log; g1t.sh when absent.
1547 #[serde(default)]
1548 pub surface: Option<crate::audit::Surface>,
1549}
1550
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.1551/// The most characters an invitation's note keeps.
1552pub const MAX_INVITE_MESSAGE: usize = 500;
1553
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)1554/// A workspace invitation waiting for its person's answer, as they see it.
1555/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1556/// newest first: pending ones only, never expired, revoked or answered.
1557#[derive(Clone, Debug, Serialize, Deserialize)]
1558#[serde(rename_all = "camelCase")]
1559pub struct WorkspaceInvitation {
1560 pub id: String,
1561 pub workspace: ProfileWorkspace,
1562 /// The role accepting joins with.
1563 pub role: crate::Role,
1564 /// Null when g1t staff sent it.
1565 pub invited_by: Option<InviteFrom>,
1566 /// RFC 3339.
1567 pub created_at: String,
1568 /// RFC 3339.
1569 pub expires_at: String,
1570}
1571
1572/// `accept_invitation`: the person it is for joins the workspace with the
1573/// role it names. Returns `Outcome<String>`, the workspace's slug.
1574///
1575/// `decline_invitation`: they say no; whoever sent it is told in their
1576/// inbox. Returns `Outcome<bool>`.
1577#[derive(Debug, Serialize, Deserialize)]
1578pub struct InvitationArgs {
1579 pub user: User,
1580 pub id: String,
1581 /// Where the request came in, for the audit log; g1t.sh when absent.
1582 #[serde(default)]
1583 pub surface: Option<crate::audit::Surface>,
1584}
1585
1586/// `find_people`: accounts whose username starts with `query`, or whose
1587/// name contains it, for picking someone to invite. Only what a profile
1588/// shows: a username, a name and an avatar, never an email address.
1589/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1590#[derive(Debug, Serialize, Deserialize)]
1591pub struct FindPeopleArgs {
1592 pub query: String,
1593 #[serde(default)]
1594 pub limit: Option<u32>,
1595}
1596
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1597/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1598/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1599///
1600/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.1601///
1602/// `resend_workspace_invite`: owners only. Sends a pending invitation
1603/// again, by email to the address it is bound to or the invited account's
1604/// confirmed address, and again in that account's inbox. The invitation
1605/// itself is unchanged: same code, same day it stops working. Returns
1606/// `Outcome<Invite>`; one that is not pending answers `409`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1607#[derive(Debug, Serialize, Deserialize)]
1608pub struct WorkspaceInviteArgs {
1609 pub actor: User,
1610 pub slug: String,
1611 pub id: String,
1612}
1613
1614/// `request_access`: someone without an invite asks for one. Kept on the
1615/// waitlist, one entry per address. Answers the same way whether or not
1616/// the address is already on it. Returns `Outcome<bool>`.
1617#[derive(Debug, Default, Serialize, Deserialize)]
1618pub struct RequestAccessArgs {
1619 pub email: String,
1620 /// What they will build, if they said.
1621 #[serde(default)]
1622 pub about: String,
1623 /// Who is asking, such as the visitor's IP address, for rate limits.
1624 #[serde(default)]
1625 pub client: Option<String>,
1626}
1627
1628/// The most characters `RequestAccessArgs::about` keeps.
1629pub const MAX_WAITLIST_ABOUT: usize = 1000;
1630
1631// `registration` takes `{}` and returns `RegistrationMode`.
1632
1633// --- Invites, staff only ---
1634
1635#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1636#[serde(rename_all = "snake_case")]
1637pub enum WaitlistStatus {
1638 Waiting,
1639 Invited,
1640 Dismissed,
1641}
1642
1643impl WaitlistStatus {
1644 pub fn as_str(self) -> &'static str {
1645 match self {
1646 WaitlistStatus::Waiting => "waiting",
1647 WaitlistStatus::Invited => "invited",
1648 WaitlistStatus::Dismissed => "dismissed",
1649 }
1650 }
1651}
1652
1653/// Someone who asked for access.
1654#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1655#[serde(rename_all = "camelCase")]
1656pub struct WaitlistEntry {
1657 pub id: String,
1658 pub email: String,
1659 pub about: Option<String>,
1660 pub status: WaitlistStatus,
1661 pub invite_id: Option<String>,
1662 pub decided_by: Option<String>,
1663 /// RFC 3339.
1664 pub decided_at: Option<String>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1665 /// What staff wrote when approving; it went in the invite email.
1666 #[serde(default)]
1667 pub note: Option<String>,
1668 /// The account made with the invite, once it was used.
1669 #[serde(default)]
1670 pub joined_as: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1671 /// When they first asked. RFC 3339.
1672 pub created_at: String,
1673 /// When they last asked. RFC 3339.
1674 pub updated_at: String,
1675}
1676
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1677/// `admin_waitlist`: the waitlist, newest first, at most
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1678/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1679///
1680/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1681/// still waiting, for sudo's navigation.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1682#[derive(Debug, Default, Serialize, Deserialize)]
1683pub struct AdminWaitlistArgs {
1684 /// Part of an email address or of what they said.
1685 #[serde(default)]
1686 pub query: Option<String>,
1687 /// Null: every status.
1688 #[serde(default)]
1689 pub status: Option<WaitlistStatus>,
1690}
1691
1692/// The most rows one staff listing of invites or the waitlist returns.
1693pub const ADMIN_INVITES_LIMIT: usize = 500;
1694
1695/// `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 quotas1696/// address, charged to nobody, and emails it, with `note` if given;
1697/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1698#[derive(Debug, Serialize, Deserialize)]
1699pub struct AdminDecideWaitlistArgs {
1700 pub id: String,
1701 pub approve: bool,
1702 /// The staff member, by email.
1703 pub staff: String,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1704 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1705 #[serde(default)]
1706 pub note: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1707}
1708
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1709/// The most characters an approval's note keeps.
1710pub const MAX_WAITLIST_NOTE: usize = 500;
1711
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1712/// `admin_invites`: every invite, newest first, at most
1713/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1714/// `query`, or whose email, inviter or redeemer contains it. Returns
1715/// `Vec<Invite>`.
1716#[derive(Debug, Default, Serialize, Deserialize)]
1717pub struct AdminInvitesArgs {
1718 #[serde(default)]
1719 pub query: Option<String>,
1720}
1721
1722/// `admin_revoke_invite`: revokes any pending invite. Returns
1723/// `Outcome<Invite>`.
1724#[derive(Debug, Serialize, Deserialize)]
1725pub struct AdminRevokeInviteArgs {
1726 pub id: String,
1727 pub staff: String,
1728}
1729
1730/// `admin_mint_invite`: staff make an invite that uses nobody's
1731/// allowance, optionally bound to (and emailed to) an address. Returns
1732/// `Outcome<Invite>`, with the code.
1733#[derive(Debug, Serialize, Deserialize)]
1734pub struct AdminMintInviteArgs {
1735 #[serde(default)]
1736 pub email: Option<String>,
1737 pub staff: String,
1738}
1739
1740/// Who staff grant invites to.
1741#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1742#[serde(rename_all = "snake_case")]
1743pub enum GrantTarget {
1744 User,
1745 Workspace,
1746}
1747
1748impl GrantTarget {
1749 pub fn as_str(self) -> &'static str {
1750 match self {
1751 GrantTarget::User => "user",
1752 GrantTarget::Workspace => "workspace",
1753 }
1754 }
1755}
1756
1757/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1758/// slug) `amount` more invites; a negative amount takes some back. Returns
1759/// `Outcome<Allowance>`: theirs afterwards.
1760#[derive(Debug, Serialize, Deserialize)]
1761pub struct AdminGrantInvitesArgs {
1762 pub target: GrantTarget,
1763 pub name: String,
1764 pub amount: i32,
1765 #[serde(default)]
1766 pub note: String,
1767 pub staff: String,
1768}
1769
1770/// The most invites one grant gives or takes back.
1771pub const MAX_INVITE_GRANT: i32 = 1000;
1772
1773/// Invites staff granted.
1774#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1775#[serde(rename_all = "camelCase")]
1776pub struct InviteGrant {
1777 pub amount: i32,
1778 pub note: Option<String>,
1779 pub granted_by: String,
1780 /// RFC 3339.
1781 pub created_at: String,
1782}
1783
1784/// Someone a person invited, and whom they invited in turn.
1785#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1786#[serde(rename_all = "camelCase")]
1787pub struct InviteTreeNode {
1788 pub username: String,
1789 /// When they used the invite. RFC 3339.
1790 pub joined_at: String,
1791 pub invited: Vec<InviteTreeNode>,
1792}
1793
1794/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1795/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1796///
1797/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1798/// invites, grants and invites. Returns `Option<InviteTree>` with
1799/// `username` the slug and no `invited_by`.
1800#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1801#[serde(rename_all = "camelCase")]
1802pub struct InviteTree {
1803 pub username: String,
1804 /// Who invited them, then who invited that person, and so on. Empty
1805 /// for an account made without an invite.
1806 pub invited_by: Vec<String>,
1807 /// The staff member who minted their invite, when staff did.
1808 pub staff: Option<String>,
1809 pub allowance: Allowance,
1810 pub grants: Vec<InviteGrant>,
1811 /// Their invites, newest first.
1812 pub invites: Vec<Invite>,
1813 /// Whom they invited, three levels down.
1814 pub invited: Vec<InviteTreeNode>,
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1815 /// The shared invite link the account was made with, if it was.
1816 #[serde(default)]
1817 pub shared: Option<SharedInviteSource>,
1818}
1819
1820// --- Shared invite links, staff only ---
1821//
1822// One link for a group (a conference's judges, a post, a community): up
1823// to `max_uses` new accounts, until it expires or staff revoke it,
1824// optionally only for addresses at some domains. Each use makes a new
1825// account, which then makes its own workspace; a shared link never joins
1826// anyone to an existing workspace, and uses nobody's allowance. Its code
1827// looks and is stored like any invite code (only a hash, and a sealed copy
1828// staff can copy again while it is live); the link is
1829// `https://g1t.sh/register?invite=<code>`. See
1830// services/identity/src/shared_invites.rs.
1831
1832/// How long a shared invite link works when staff give no date.
1833pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1834/// The furthest ahead a shared invite link's last day may be set.
1835pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1836/// The most accounts one shared invite link makes.
1837pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1838/// The most characters a shared invite link's label keeps.
1839pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1840/// The most email domains one shared invite link may be limited to.
1841pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1842
1843/// Where a shared invite link stands. Only a live one makes accounts.
1844#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1845#[serde(rename_all = "snake_case")]
1846pub enum SharedInviteStatus {
1847 Live,
1848 /// Every use is taken.
1849 UsedUp,
1850 Expired,
1851 Revoked,
1852}
1853
1854/// The shared invite link an account was made with.
1855#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1856pub struct SharedInviteSource {
1857 pub id: String,
1858 pub label: String,
1859}
1860
1861/// An account made with a shared invite link.
1862#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1863#[serde(rename_all = "camelCase")]
1864pub struct SharedInviteAccount {
1865 /// Null once the account is purged.
1866 pub username: Option<String>,
1867 /// When it was made with the link. RFC 3339.
1868 pub joined_at: String,
1869}
1870
1871/// One shared invite link, as staff see it.
1872#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1873#[serde(rename_all = "camelCase")]
1874pub struct SharedInvite {
1875 /// `sinv_…`.
1876 pub id: String,
1877 /// Whom it is for, such as `Cloudflare judges`.
1878 pub label: String,
1879 /// The code, while it is live (and IDENTITY_KEY is set).
1880 pub code: Option<String>,
1881 /// The code's first group, such as `g1t-k7m2`.
1882 pub hint: String,
1883 pub max_uses: u32,
1884 /// Accounts made with it so far.
1885 pub uses: u32,
1886 /// Only addresses at these domains may use it; empty for any.
1887 pub domains: Vec<String>,
1888 pub status: SharedInviteStatus,
1889 /// The staff member who made it, by email.
1890 pub staff: String,
1891 /// RFC 3339.
1892 pub created_at: String,
1893 /// RFC 3339.
1894 pub expires_at: String,
1895 pub revoked_at: Option<String>,
1896 pub revoked_by: Option<String>,
1897 /// The accounts made with it, oldest first.
1898 pub accounts: Vec<SharedInviteAccount>,
1899}
1900
1901/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1902/// at most 200, each with the accounts it made. Returns
1903/// `Vec<SharedInvite>`.
1904///
1905/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1906/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1907#[derive(Debug, Default, Serialize, Deserialize)]
1908pub struct AdminCreateSharedInviteArgs {
1909 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1910 pub label: String,
1911 /// 1 to [`MAX_SHARED_INVITE_USES`].
1912 pub max_uses: u32,
1913 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1914 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1915 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1916 #[serde(default)]
1917 pub expires_on: Option<String>,
1918 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1919 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1920 #[serde(default)]
1921 pub domains: Vec<String>,
1922 /// The staff member, by email.
1923 pub staff: String,
1924}
1925
1926/// `admin_revoke_shared_invite`: stops a shared invite link making any
1927/// more accounts. Those it made stay. Recorded in sudo's audit log.
1928/// Returns `Outcome<SharedInvite>`.
1929#[derive(Debug, Serialize, Deserialize)]
1930pub struct AdminRevokeSharedInviteArgs {
1931 pub id: String,
1932 pub staff: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1933}
1934
1935#[cfg(test)]
1936mod deletion_tests {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1937 use super::{WorkspaceDeletion, protected_names};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1938
1939 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1940 fn only_billing_or_protection_stands_in_the_way() {
1941 let clear = WorkspaceDeletion {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1942 repositories: 2,
1943 projects: 1,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1944 members: 3,
1945 ..WorkspaceDeletion::default()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1946 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1947 assert!(!clear.blocked());
1948 assert_eq!(clear.reason("acme"), None);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1949 let owing = WorkspaceDeletion {
1950 billing: Some("Pay first.".into()),
1951 ..WorkspaceDeletion::default()
1952 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1953 assert!(owing.blocked());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1954 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 member1955 let protected = WorkspaceDeletion {
1956 billing: Some("Pay first.".into()),
1957 protected: true,
1958 ..WorkspaceDeletion::default()
1959 };
1960 assert!(protected.blocked());
1961 assert_eq!(
1962 protected.reason("flagon-io").as_deref(),
1963 Some("flagon-io is protected and can never be deleted.")
1964 );
1965 }
1966
1967 #[test]
1968 fn flagon_is_protected_whatever_the_variable_says() {
1969 assert_eq!(protected_names(None), ["flagon-io"]);
1970 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1971 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1972 assert_eq!(
1973 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1974 ["flagon-io", "acme", "wsp_1"]
1975 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1976 }
1977}

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