Skip to content
1,844 linesCodeBlameRaw
1//! The identity service: accounts, credentials and sessions.
2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::User;
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SshKey {
13 pub id: String,
14 pub title: String,
15 pub fingerprint: String,
16 /// RFC 3339.
17 pub created_at: String,
18 /// 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>,
22}
23
24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
25#[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>,
36 /// Its scopes, as `resource:level`, the highest of each resource.
37 /// Null: full access (an application's or an agent's credential).
38 #[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>,
46 /// 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.
49 #[serde(default)]
50 pub permissions: std::collections::BTreeMap<String, String>,
51 /// What it is for, as its owner wrote it.
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub description: Option<String>,
54 /// 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.
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 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.
79 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
80 pub admin: bool,
81 /// 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,
85}
86
87/// `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`).
91/// Returns `Outcome<SignedIn>`.
92#[derive(Debug, Serialize, Deserialize)]
93pub struct SignInArgs {
94 pub username: String,
95 pub password: String,
96 /// Who is asking, such as the visitor's IP address, for rate limits.
97 #[serde(default)]
98 pub client: Option<String>,
99}
100
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct SignedIn {
104 pub user: User,
105 /// Empty while `two_factor_challenge` is set: no session is made until
106 /// the code is given.
107 pub session_token: String,
108 /// 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>,
113}
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
135/// `user_for_ssh_key`, and `principal_for_ssh_key` (see [`crate::deploy_keys`]).
136#[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
149/// not know are left out. Also `accounts`: the accounts behind user ids,
150/// each with its username and avatar (`HashMap<String, accounts::EmailOwner>`).
151#[derive(Debug, Serialize, Deserialize)]
152pub struct UsernamesArgs {
153 pub ids: Vec<String>,
154}
155
156/// `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
165/// `list_ssh_keys` and `list_access_tokens`.
166#[derive(Debug, Serialize, Deserialize)]
167pub struct UserArgs {
168 pub user: User,
169}
170
171/// `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
180/// `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
206 /// left out of the user's token list, unless `listed`. Used for hosted
207 /// attempts.
208 #[serde(default)]
209 pub ttl_seconds: Option<u64>,
210 /// 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
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
251/// 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>`.
260///
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`].
264#[derive(Debug, Serialize, Deserialize)]
265pub struct RegisterArgs {
266 pub username: String,
267 pub email: String,
268 pub password: String,
269 /// An invite code such as `g1t-k7m2-q9xd-4hpw-…`. Ignored while
270 /// registration is open.
271 #[serde(default)]
272 pub invite_code: Option<String>,
273 /// 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>,
278 /// Who is asking, such as the visitor's IP address, for rate limits.
279 #[serde(default)]
280 pub client: Option<String>,
281}
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
290/// 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.
294#[derive(Debug, Serialize, Deserialize)]
295pub struct EmailArgs {
296 pub email: String,
297 /// Who is asking, such as the visitor's IP address, for rate limits.
298 #[serde(default)]
299 pub client: Option<String>,
300}
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,
392 /// 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>,
396 /// 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,
400 /// Who may create its teams. See [`crate::teams::TeamCreation`].
401 #[serde(default)]
402 pub team_creation: crate::teams::TeamCreation,
403 /// 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,
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct Member {
416 pub username: String,
417 /// 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>,
421 pub role: crate::Role,
422 /// 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>,
429 /// 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>,
436}
437
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
477/// `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
492/// `list_members`: members only. Owners also see each member's
493/// `two_factor`. Returns `Outcome<Vec<Member>>`.
494#[derive(Debug, Serialize, Deserialize)]
495pub struct ListMembersArgs {
496 pub slug: String,
497 pub viewer: crate::Viewer,
498}
499
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
504/// leaving (`members::LeaveWorkspaceArgs`); removing an owner is refused
505/// when they are the last. Each returns `Outcome<bool>`.
506#[derive(Debug, Serialize, Deserialize)]
507pub struct MemberArgs {
508 pub actor: User,
509 pub slug: String,
510 pub username: String,
511 #[serde(default)]
512 pub surface: Option<crate::audit::Surface>,
513}
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
525/// `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
541/// `delete_workspace`: owners only, and only a person. `confirm` must be
542/// 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>`.
553///
554/// `check_workspace_deletion` takes the same arguments (with `confirm`
555/// ignored) and says what would go and whether anything stands in the way,
556/// changing nothing. Returns `Outcome<WorkspaceDeletion>`.
557#[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
568/// 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`.
570#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
571pub struct WorkspaceDeletion {
572 /// Its live repositories, which are deleted with it.
573 pub repositories: u32,
574 /// Its projects, hidden with it.
575 pub projects: u32,
576 #[serde(default)]
577 pub members: u32,
578 /// Why billing cannot close the workspace yet, in words for its owner.
579 pub billing: Option<String>,
580 /// It can never be deleted, by anyone ([`protected_names`]).
581 #[serde(default)]
582 pub protected: bool,
583}
584
585impl WorkspaceDeletion {
586 pub fn blocked(&self) -> bool {
587 self.protected || self.billing.is_some()
588 }
589
590 /// Why the workspace cannot be deleted, as one sentence, or `None`.
591 pub fn reason(&self, slug: &str) -> Option<String> {
592 if self.protected {
593 return Some(protected_refusal(slug));
594 }
595 self.billing.clone()
596 }
597}
598
599/// 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,
641 /// 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.
643 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
667/// `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
676/// 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
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}
739
740/// `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
761/// `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,
790 /// What the person granted, as `resource:level`. Null: full access.
791 #[serde(default)]
792 pub scopes: Option<Vec<String>>,
793}
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,
828 /// The scopes granted, space-separated, or `*` for full access.
829 #[serde(default)]
830 pub scope: Option<String>,
831}
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,
844 /// 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>>,
861}
862
863
864/// What an agent's token may do: these operations, in this repository.
865#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
866pub struct AgentScope {
867 pub repo: crate::repos::RepoPath,
868 /// API and MCP operation names, such as `create_issue`.
869 pub operations: Vec<String>,
870 /// 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>,
874}
875
876/// `create_agent_token`: a token for a g1t agent working on someone's
877/// behalf. It acts as `g1t`, a member of the repository's workspace,
878/// 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
891/// 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.
894pub const AGENT_ID: &str = "usr_g1t_agent";
895/// 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;
898
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>,
968 /// It can never be deleted ([`protected_names`]).
969 #[serde(default)]
970 pub protected: bool,
971}
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}
982
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;
994pub const MAX_PROFILE_TIMEZONE: usize = 64;
995
996/// What anyone may see about a person.
997#[derive(Clone, Debug, Default, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct Profile {
1000 /// Lowercased: what the profile is found and linked by.
1001 pub username: String,
1002 /// 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>,
1005 /// 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>,
1013 /// The time zone they are in, an IANA name such as `America/Denver`.
1014 #[serde(default)]
1015 pub timezone: Option<String>,
1016 /// 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,
1041 /// An IANA time zone name, such as `America/Denver`.
1042 #[serde(default)]
1043 pub timezone: String,
1044}
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}
1067
1068/// `directory`: every account or every workspace, as their public pages
1069/// show them, a page at a time in name order. For services that index
1070/// them, such as search; nothing private is in it. Returns
1071/// `DirectoryPage`.
1072#[derive(Debug, Default, Serialize, Deserialize)]
1073pub struct DirectoryArgs {
1074 /// `user` or `workspace`.
1075 pub kind: String,
1076 /// Names after this one.
1077 #[serde(default)]
1078 pub after: Option<String>,
1079 pub limit: u32,
1080}
1081
1082/// One account or workspace in the directory.
1083#[derive(Clone, Debug, Serialize, Deserialize)]
1084#[serde(rename_all = "camelCase")]
1085pub struct DirectoryEntry {
1086 /// The account's or workspace's id.
1087 pub id: String,
1088 /// A username or a workspace's slug.
1089 pub slug: String,
1090 /// A person's display name or a workspace's name.
1091 pub name: Option<String>,
1092 /// A person's bio or a workspace's description.
1093 pub bio: Option<String>,
1094 pub avatar: Option<String>,
1095 /// RFC 3339.
1096 pub created_at: String,
1097}
1098
1099#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1100pub struct DirectoryPage {
1101 pub entries: Vec<DirectoryEntry>,
1102 /// Where the next page starts; null on the last.
1103 pub next: Option<String>,
1104}
1105
1106// --- Invites ---------------------------------------------------------------
1107//
1108// While registration is invite-only, every new account (with a password or
1109// through GitHub) needs an invite code. Each person may have
1110// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1111// to a workspace, whose owners share them. Inviting an email with no
1112// account into a workspace makes an invite bound to that address, which
1113// registers and joins in one step. See services/identity/src/invites.rs.
1114
1115/// Whether anyone may make an account, or only someone with an invite. Set
1116/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1117/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1118#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1119#[serde(rename_all = "snake_case")]
1120pub enum RegistrationMode {
1121 #[default]
1122 Invite,
1123 Open,
1124}
1125
1126impl RegistrationMode {
1127 pub fn parse(text: Option<&str>) -> RegistrationMode {
1128 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1129 Some("open") => RegistrationMode::Open,
1130 _ => RegistrationMode::Invite,
1131 }
1132 }
1133}
1134
1135/// How many invites a person may have out at once, unless identity's
1136/// `INVITES_PER_USER` var says otherwise.
1137pub const INVITES_PER_USER: u32 = 5;
1138
1139/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1140/// otherwise.
1141pub const INVITE_TTL_DAYS: u64 = 30;
1142
1143/// Where an invite stands. Only a pending invite can be used. A pending
1144/// invite can be revoked, and so can one awaiting confirmation. An expired
1145/// or revoked invite that was never used gives its inviter the invite back.
1146#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1147#[serde(rename_all = "snake_case")]
1148pub enum InviteStatus {
1149 Pending,
1150 /// Used to make an account that has not confirmed its email address
1151 /// yet. The code is spent; the workspace (or repository) it gives is
1152 /// joined when the address is confirmed, unless it is revoked first.
1153 AwaitingConfirmation,
1154 /// Used to make an account that has confirmed its address, for a
1155 /// workspace it has not yet joined or declined: the workspace
1156 /// invitation waits for the person's answer (`accept_invitation`).
1157 AwaitingAnswer,
1158 Redeemed,
1159 /// Its person declined the workspace it invited them to.
1160 Declined,
1161 Expired,
1162 Revoked,
1163}
1164
1165/// What using an invite does.
1166#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1167#[serde(rename_all = "snake_case")]
1168pub enum InviteKind {
1169 /// Makes a new account, and joins `workspace` when one is set.
1170 Account,
1171 /// An existing account joins `workspace`. Never makes an account.
1172 Workspace,
1173}
1174
1175/// Whose allowance an invite uses.
1176#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1177#[serde(rename_all = "snake_case")]
1178pub enum InviteCharge {
1179 /// Its inviter's own.
1180 User,
1181 /// The workspace's, granted by staff and shared by its owners.
1182 Workspace,
1183 /// Nobody's: staff minted it, or it invites an existing account.
1184 None,
1185}
1186
1187/// One invite, as the person who made it sees it.
1188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1189#[serde(rename_all = "camelCase")]
1190pub struct Invite {
1191 pub id: String,
1192 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1193 /// is made, and afterwards to whoever made it while it is pending.
1194 /// Null otherwise.
1195 pub code: Option<String>,
1196 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1197 pub hint: String,
1198 /// Only an account with this address can use it. Null: anyone with
1199 /// the code.
1200 pub email: Option<String>,
1201 pub kind: InviteKind,
1202 /// The workspace it joins, by slug.
1203 pub workspace: Option<String>,
1204 pub status: InviteStatus,
1205 pub charged_to: InviteCharge,
1206 /// Who made it, by username. Null when g1t staff did.
1207 pub invited_by: Option<String>,
1208 /// The account that used it, by username.
1209 pub redeemed_by: Option<String>,
1210 /// RFC 3339.
1211 pub created_at: String,
1212 /// RFC 3339.
1213 pub expires_at: String,
1214 /// RFC 3339.
1215 pub redeemed_at: Option<String>,
1216 /// RFC 3339.
1217 pub revoked_at: Option<String>,
1218 /// The account a workspace invitation is for, by username: someone
1219 /// invited by username, or the account the invite made.
1220 #[serde(default)]
1221 pub invitee: Option<String>,
1222 /// The role `workspace` is joined with. Null when it names none.
1223 #[serde(default)]
1224 pub role: Option<crate::Role>,
1225 /// The staff member who minted it. Only in staff views.
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1227 pub staff: Option<String>,
1228}
1229
1230/// How many invites someone may have out, and how many they have.
1231#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1232pub struct Allowance {
1233 /// Null: no limit.
1234 pub limit: Option<u32>,
1235 /// Pending and used invites; revoked and expired ones are not counted.
1236 pub used: u32,
1237 /// Null: no limit.
1238 pub remaining: Option<u32>,
1239}
1240
1241impl Allowance {
1242 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1243 Allowance {
1244 limit,
1245 used,
1246 remaining: limit.map(|limit| limit.saturating_sub(used)),
1247 }
1248 }
1249
1250 pub fn exhausted(&self) -> bool {
1251 self.remaining == Some(0)
1252 }
1253}
1254
1255/// A workspace's shared invites, for one of its owners.
1256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1257pub struct WorkspaceAllowance {
1258 pub slug: String,
1259 pub allowance: Allowance,
1260}
1261
1262/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1263/// and what they have left. Returns `InvitesOverview`.
1264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1265pub struct InvitesOverview {
1266 pub mode: RegistrationMode,
1267 pub allowance: Allowance,
1268 /// Workspaces the person owns that staff granted invites to.
1269 pub workspaces: Vec<WorkspaceAllowance>,
1270 pub invites: Vec<Invite>,
1271}
1272
1273/// `create_invite`: a person makes an invite, optionally for one email
1274/// address. People only; never an agent or a workspace's token, and not
1275/// before their email is confirmed. Uses one of the person's invites, or,
1276/// with `workspace`, one of the invites staff granted that workspace (its
1277/// owners only). Emails the address when one is given. Returns
1278/// `Outcome<Invite>`, with the code.
1279///
1280/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1281/// invite; a workspace's owners may revoke one made for the workspace.
1282/// The invite comes back to whoever it was charged to. Returns
1283/// `Outcome<Invite>`.
1284#[derive(Debug, Serialize, Deserialize)]
1285pub struct CreateInviteArgs {
1286 pub user: User,
1287 #[serde(default)]
1288 pub email: Option<String>,
1289 /// Use this workspace's granted invites, by slug.
1290 #[serde(default)]
1291 pub workspace: Option<String>,
1292 /// The workspace the new account is invited to, by slug: one the
1293 /// person owns that can add members (not on the free plan). Once the
1294 /// account is confirmed it gets a workspace invitation to accept, as a
1295 /// member, and no workspace of its own is made for it.
1296 #[serde(default)]
1297 pub join: Option<String>,
1298 /// The role `join` invites them with; member when absent. Ignored
1299 /// without `join`.
1300 #[serde(default)]
1301 pub join_role: Option<crate::Role>,
1302 /// Where the request came in, for the audit log; g1t.sh when absent.
1303 #[serde(default)]
1304 pub surface: Option<crate::audit::Surface>,
1305}
1306
1307/// `check_invite`: what an invite code is for, before using it. Returns
1308/// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or
1309/// expired gets the same answer, so codes cannot be probed. With
1310/// `any_status`, a real code that can no longer be used is described
1311/// instead (its `status` says why), so the page can say whom to ask for a
1312/// new one; an unknown code still gets the one answer.
1313#[derive(Debug, Serialize, Deserialize)]
1314pub struct InviteCodeArgs {
1315 pub code: String,
1316 /// Who is asking, such as the visitor's IP address, for rate limits.
1317 #[serde(default)]
1318 pub client: Option<String>,
1319 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1320 #[serde(default)]
1321 pub viewer: Option<User>,
1322 #[serde(default)]
1323 pub any_status: bool,
1324 /// The `proof` from the invite email's link, if the page was opened
1325 /// from it: sets `InvitePreview::email_proven`.
1326 #[serde(default)]
1327 pub email_proof: Option<String>,
1328}
1329
1330/// Someone shown on an invite.
1331#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1332pub struct InviteFrom {
1333 pub username: String,
1334 pub name: Option<String>,
1335 pub avatar: Option<String>,
1336}
1337
1338/// A repository an invite code was sent with: using the code accepts the
1339/// invitation to collaborate on it.
1340#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1341pub struct InviteRepository {
1342 /// `workspace/repo`.
1343 pub name: String,
1344 /// The role it gives, such as `write`.
1345 pub role: String,
1346}
1347
1348/// What a valid invite code is for.
1349#[derive(Clone, Debug, Serialize, Deserialize)]
1350#[serde(rename_all = "camelCase")]
1351pub struct InvitePreview {
1352 pub kind: InviteKind,
1353 /// Pending, unless `any_status` asked about a code that is spent.
1354 pub status: InviteStatus,
1355 /// Null when g1t staff sent it.
1356 pub invited_by: Option<InviteFrom>,
1357 pub workspace: Option<ProfileWorkspace>,
1358 /// The repository it accepts an invitation to, if it was sent with one.
1359 pub repository: Option<InviteRepository>,
1360 /// The address it is for, partly hidden, such as `a•••@example.com`.
1361 pub email: Option<String>,
1362 /// The address in full, while it is pending: whoever holds the code
1363 /// was sent it there. Fills in and locks the sign-up form.
1364 pub address: Option<String>,
1365 /// Whether the address it is for has a g1t account already, so the
1366 /// page asks them to sign in rather than sign up.
1367 pub has_account: bool,
1368 /// With a viewer: whether the invite is theirs (it is for one of their
1369 /// confirmed addresses, or they used it). Null without a viewer or,
1370 /// for a pending invite, when it is for anyone with the code.
1371 pub for_viewer: Option<bool>,
1372 /// RFC 3339.
1373 pub expires_at: String,
1374 /// For a shared invite link ([`SharedInvite`]): the group it was made
1375 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1376 /// it. Null for a one-person invite.
1377 #[serde(default)]
1378 pub shared_label: Option<String>,
1379 /// For a shared invite link limited to some email domains: those
1380 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1381 #[serde(default)]
1382 pub shared_domains: Vec<String>,
1383 /// Whether `email_proof` was this pending invite's own, from the email
1384 /// it was sent in: the account made with it starts with `address`
1385 /// confirmed. False without a proof, with a wrong one, and for an
1386 /// invite bound to no address.
1387 #[serde(default)]
1388 pub email_proven: bool,
1389}
1390
1391/// `accept_invite`: a signed-in person uses a workspace invite made for
1392/// their confirmed address, and joins the workspace, or an invite sent with
1393/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1394/// workspace's slug, or `workspace/repo`.
1395#[derive(Debug, Serialize, Deserialize)]
1396pub struct AcceptInviteArgs {
1397 pub user: User,
1398 pub code: String,
1399}
1400
1401/// `invite_member`: an owner invites an email address into a workspace.
1402/// It always makes an invite bound to that address and emails it, so the
1403/// answer never says whether the address has an account. Without one, the
1404/// invite registers and joins in one step, and uses one of the workspace's
1405/// granted invites or else one of the owner's own. With one, it costs
1406/// nothing. Returns `Outcome<Invite>`, with the code.
1407#[derive(Debug, Serialize, Deserialize)]
1408pub struct InviteMemberArgs {
1409 pub actor: User,
1410 pub slug: String,
1411 /// An email address. Give this or `username`.
1412 #[serde(default)]
1413 pub email: String,
1414 /// A g1t username: that account gets a workspace invitation to accept
1415 /// or decline, in its inbox and by email. Nobody joins without saying
1416 /// yes.
1417 #[serde(default)]
1418 pub username: Option<String>,
1419 /// The role they join with; member when absent.
1420 #[serde(default)]
1421 pub role: Option<crate::Role>,
1422 /// 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/// A workspace invitation waiting for its person's answer, as they see it.
1428/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1429/// newest first: pending ones only, never expired, revoked or answered.
1430#[derive(Clone, Debug, Serialize, Deserialize)]
1431#[serde(rename_all = "camelCase")]
1432pub struct WorkspaceInvitation {
1433 pub id: String,
1434 pub workspace: ProfileWorkspace,
1435 /// The role accepting joins with.
1436 pub role: crate::Role,
1437 /// Null when g1t staff sent it.
1438 pub invited_by: Option<InviteFrom>,
1439 /// RFC 3339.
1440 pub created_at: String,
1441 /// RFC 3339.
1442 pub expires_at: String,
1443}
1444
1445/// `accept_invitation`: the person it is for joins the workspace with the
1446/// role it names. Returns `Outcome<String>`, the workspace's slug.
1447///
1448/// `decline_invitation`: they say no; whoever sent it is told in their
1449/// inbox. Returns `Outcome<bool>`.
1450#[derive(Debug, Serialize, Deserialize)]
1451pub struct InvitationArgs {
1452 pub user: User,
1453 pub id: String,
1454 /// Where the request came in, for the audit log; g1t.sh when absent.
1455 #[serde(default)]
1456 pub surface: Option<crate::audit::Surface>,
1457}
1458
1459/// `find_people`: accounts whose username starts with `query`, or whose
1460/// name contains it, for picking someone to invite. Only what a profile
1461/// shows: a username, a name and an avatar, never an email address.
1462/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1463#[derive(Debug, Serialize, Deserialize)]
1464pub struct FindPeopleArgs {
1465 pub query: String,
1466 #[serde(default)]
1467 pub limit: Option<u32>,
1468}
1469
1470/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1471/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1472///
1473/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1474#[derive(Debug, Serialize, Deserialize)]
1475pub struct WorkspaceInviteArgs {
1476 pub actor: User,
1477 pub slug: String,
1478 pub id: String,
1479}
1480
1481/// `request_access`: someone without an invite asks for one. Kept on the
1482/// waitlist, one entry per address. Answers the same way whether or not
1483/// the address is already on it. Returns `Outcome<bool>`.
1484#[derive(Debug, Default, Serialize, Deserialize)]
1485pub struct RequestAccessArgs {
1486 pub email: String,
1487 /// What they will build, if they said.
1488 #[serde(default)]
1489 pub about: String,
1490 /// Who is asking, such as the visitor's IP address, for rate limits.
1491 #[serde(default)]
1492 pub client: Option<String>,
1493}
1494
1495/// The most characters `RequestAccessArgs::about` keeps.
1496pub const MAX_WAITLIST_ABOUT: usize = 1000;
1497
1498// `registration` takes `{}` and returns `RegistrationMode`.
1499
1500// --- Invites, staff only ---
1501
1502#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1503#[serde(rename_all = "snake_case")]
1504pub enum WaitlistStatus {
1505 Waiting,
1506 Invited,
1507 Dismissed,
1508}
1509
1510impl WaitlistStatus {
1511 pub fn as_str(self) -> &'static str {
1512 match self {
1513 WaitlistStatus::Waiting => "waiting",
1514 WaitlistStatus::Invited => "invited",
1515 WaitlistStatus::Dismissed => "dismissed",
1516 }
1517 }
1518}
1519
1520/// Someone who asked for access.
1521#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1522#[serde(rename_all = "camelCase")]
1523pub struct WaitlistEntry {
1524 pub id: String,
1525 pub email: String,
1526 pub about: Option<String>,
1527 pub status: WaitlistStatus,
1528 pub invite_id: Option<String>,
1529 pub decided_by: Option<String>,
1530 /// RFC 3339.
1531 pub decided_at: Option<String>,
1532 /// What staff wrote when approving; it went in the invite email.
1533 #[serde(default)]
1534 pub note: Option<String>,
1535 /// The account made with the invite, once it was used.
1536 #[serde(default)]
1537 pub joined_as: Option<String>,
1538 /// When they first asked. RFC 3339.
1539 pub created_at: String,
1540 /// When they last asked. RFC 3339.
1541 pub updated_at: String,
1542}
1543
1544/// `admin_waitlist`: the waitlist, newest first, at most
1545/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
1546///
1547/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1548/// still waiting, for sudo's navigation.
1549#[derive(Debug, Default, Serialize, Deserialize)]
1550pub struct AdminWaitlistArgs {
1551 /// Part of an email address or of what they said.
1552 #[serde(default)]
1553 pub query: Option<String>,
1554 /// Null: every status.
1555 #[serde(default)]
1556 pub status: Option<WaitlistStatus>,
1557}
1558
1559/// The most rows one staff listing of invites or the waitlist returns.
1560pub const ADMIN_INVITES_LIMIT: usize = 500;
1561
1562/// `admin_decide_waitlist`: approving mints an invite bound to the
1563/// address, charged to nobody, and emails it, with `note` if given;
1564/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
1565#[derive(Debug, Serialize, Deserialize)]
1566pub struct AdminDecideWaitlistArgs {
1567 pub id: String,
1568 pub approve: bool,
1569 /// The staff member, by email.
1570 pub staff: String,
1571 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1572 #[serde(default)]
1573 pub note: Option<String>,
1574}
1575
1576/// The most characters an approval's note keeps.
1577pub const MAX_WAITLIST_NOTE: usize = 500;
1578
1579/// `admin_invites`: every invite, newest first, at most
1580/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1581/// `query`, or whose email, inviter or redeemer contains it. Returns
1582/// `Vec<Invite>`.
1583#[derive(Debug, Default, Serialize, Deserialize)]
1584pub struct AdminInvitesArgs {
1585 #[serde(default)]
1586 pub query: Option<String>,
1587}
1588
1589/// `admin_revoke_invite`: revokes any pending invite. Returns
1590/// `Outcome<Invite>`.
1591#[derive(Debug, Serialize, Deserialize)]
1592pub struct AdminRevokeInviteArgs {
1593 pub id: String,
1594 pub staff: String,
1595}
1596
1597/// `admin_mint_invite`: staff make an invite that uses nobody's
1598/// allowance, optionally bound to (and emailed to) an address. Returns
1599/// `Outcome<Invite>`, with the code.
1600#[derive(Debug, Serialize, Deserialize)]
1601pub struct AdminMintInviteArgs {
1602 #[serde(default)]
1603 pub email: Option<String>,
1604 pub staff: String,
1605}
1606
1607/// Who staff grant invites to.
1608#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1609#[serde(rename_all = "snake_case")]
1610pub enum GrantTarget {
1611 User,
1612 Workspace,
1613}
1614
1615impl GrantTarget {
1616 pub fn as_str(self) -> &'static str {
1617 match self {
1618 GrantTarget::User => "user",
1619 GrantTarget::Workspace => "workspace",
1620 }
1621 }
1622}
1623
1624/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1625/// slug) `amount` more invites; a negative amount takes some back. Returns
1626/// `Outcome<Allowance>`: theirs afterwards.
1627#[derive(Debug, Serialize, Deserialize)]
1628pub struct AdminGrantInvitesArgs {
1629 pub target: GrantTarget,
1630 pub name: String,
1631 pub amount: i32,
1632 #[serde(default)]
1633 pub note: String,
1634 pub staff: String,
1635}
1636
1637/// The most invites one grant gives or takes back.
1638pub const MAX_INVITE_GRANT: i32 = 1000;
1639
1640/// Invites staff granted.
1641#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1642#[serde(rename_all = "camelCase")]
1643pub struct InviteGrant {
1644 pub amount: i32,
1645 pub note: Option<String>,
1646 pub granted_by: String,
1647 /// RFC 3339.
1648 pub created_at: String,
1649}
1650
1651/// Someone a person invited, and whom they invited in turn.
1652#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1653#[serde(rename_all = "camelCase")]
1654pub struct InviteTreeNode {
1655 pub username: String,
1656 /// When they used the invite. RFC 3339.
1657 pub joined_at: String,
1658 pub invited: Vec<InviteTreeNode>,
1659}
1660
1661/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1662/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1663///
1664/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1665/// invites, grants and invites. Returns `Option<InviteTree>` with
1666/// `username` the slug and no `invited_by`.
1667#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1668#[serde(rename_all = "camelCase")]
1669pub struct InviteTree {
1670 pub username: String,
1671 /// Who invited them, then who invited that person, and so on. Empty
1672 /// for an account made without an invite.
1673 pub invited_by: Vec<String>,
1674 /// The staff member who minted their invite, when staff did.
1675 pub staff: Option<String>,
1676 pub allowance: Allowance,
1677 pub grants: Vec<InviteGrant>,
1678 /// Their invites, newest first.
1679 pub invites: Vec<Invite>,
1680 /// Whom they invited, three levels down.
1681 pub invited: Vec<InviteTreeNode>,
1682 /// The shared invite link the account was made with, if it was.
1683 #[serde(default)]
1684 pub shared: Option<SharedInviteSource>,
1685}
1686
1687// --- Shared invite links, staff only ---
1688//
1689// One link for a group (a conference's judges, a post, a community): up
1690// to `max_uses` new accounts, until it expires or staff revoke it,
1691// optionally only for addresses at some domains. Each use makes a new
1692// account, which then makes its own workspace; a shared link never joins
1693// anyone to an existing workspace, and uses nobody's allowance. Its code
1694// looks and is stored like any invite code (only a hash, and a sealed copy
1695// staff can copy again while it is live); the link is
1696// `https://g1t.sh/register?invite=<code>`. See
1697// services/identity/src/shared_invites.rs.
1698
1699/// How long a shared invite link works when staff give no date.
1700pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1701/// The furthest ahead a shared invite link's last day may be set.
1702pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1703/// The most accounts one shared invite link makes.
1704pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1705/// The most characters a shared invite link's label keeps.
1706pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1707/// The most email domains one shared invite link may be limited to.
1708pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1709
1710/// Where a shared invite link stands. Only a live one makes accounts.
1711#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1712#[serde(rename_all = "snake_case")]
1713pub enum SharedInviteStatus {
1714 Live,
1715 /// Every use is taken.
1716 UsedUp,
1717 Expired,
1718 Revoked,
1719}
1720
1721/// The shared invite link an account was made with.
1722#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1723pub struct SharedInviteSource {
1724 pub id: String,
1725 pub label: String,
1726}
1727
1728/// An account made with a shared invite link.
1729#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1730#[serde(rename_all = "camelCase")]
1731pub struct SharedInviteAccount {
1732 /// Null once the account is purged.
1733 pub username: Option<String>,
1734 /// When it was made with the link. RFC 3339.
1735 pub joined_at: String,
1736}
1737
1738/// One shared invite link, as staff see it.
1739#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1740#[serde(rename_all = "camelCase")]
1741pub struct SharedInvite {
1742 /// `sinv_…`.
1743 pub id: String,
1744 /// Whom it is for, such as `Cloudflare judges`.
1745 pub label: String,
1746 /// The code, while it is live (and IDENTITY_KEY is set).
1747 pub code: Option<String>,
1748 /// The code's first group, such as `g1t-k7m2`.
1749 pub hint: String,
1750 pub max_uses: u32,
1751 /// Accounts made with it so far.
1752 pub uses: u32,
1753 /// Only addresses at these domains may use it; empty for any.
1754 pub domains: Vec<String>,
1755 pub status: SharedInviteStatus,
1756 /// The staff member who made it, by email.
1757 pub staff: String,
1758 /// RFC 3339.
1759 pub created_at: String,
1760 /// RFC 3339.
1761 pub expires_at: String,
1762 pub revoked_at: Option<String>,
1763 pub revoked_by: Option<String>,
1764 /// The accounts made with it, oldest first.
1765 pub accounts: Vec<SharedInviteAccount>,
1766}
1767
1768/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1769/// at most 200, each with the accounts it made. Returns
1770/// `Vec<SharedInvite>`.
1771///
1772/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1773/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1774#[derive(Debug, Default, Serialize, Deserialize)]
1775pub struct AdminCreateSharedInviteArgs {
1776 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1777 pub label: String,
1778 /// 1 to [`MAX_SHARED_INVITE_USES`].
1779 pub max_uses: u32,
1780 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1781 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1782 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1783 #[serde(default)]
1784 pub expires_on: Option<String>,
1785 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1786 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1787 #[serde(default)]
1788 pub domains: Vec<String>,
1789 /// The staff member, by email.
1790 pub staff: String,
1791}
1792
1793/// `admin_revoke_shared_invite`: stops a shared invite link making any
1794/// more accounts. Those it made stay. Recorded in sudo's audit log.
1795/// Returns `Outcome<SharedInvite>`.
1796#[derive(Debug, Serialize, Deserialize)]
1797pub struct AdminRevokeSharedInviteArgs {
1798 pub id: String,
1799 pub staff: String,
1800}
1801
1802#[cfg(test)]
1803mod deletion_tests {
1804 use super::{WorkspaceDeletion, protected_names};
1805
1806 #[test]
1807 fn only_billing_or_protection_stands_in_the_way() {
1808 let clear = WorkspaceDeletion {
1809 repositories: 2,
1810 projects: 1,
1811 members: 3,
1812 ..WorkspaceDeletion::default()
1813 };
1814 assert!(!clear.blocked());
1815 assert_eq!(clear.reason("acme"), None);
1816 let owing = WorkspaceDeletion {
1817 billing: Some("Pay first.".into()),
1818 ..WorkspaceDeletion::default()
1819 };
1820 assert!(owing.blocked());
1821 assert_eq!(owing.reason("acme").as_deref(), Some("Pay first."));
1822 let protected = WorkspaceDeletion {
1823 billing: Some("Pay first.".into()),
1824 protected: true,
1825 ..WorkspaceDeletion::default()
1826 };
1827 assert!(protected.blocked());
1828 assert_eq!(
1829 protected.reason("flagon-io").as_deref(),
1830 Some("flagon-io is protected and can never be deleted.")
1831 );
1832 }
1833
1834 #[test]
1835 fn flagon_is_protected_whatever_the_variable_says() {
1836 assert_eq!(protected_names(None), ["flagon-io"]);
1837 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1838 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1839 assert_eq!(
1840 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1841 ["flagon-io", "acme", "wsp_1"]
1842 );
1843 }
1844}