Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Webhooks: every event, to your own addresses, signed and retried | 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 | ||
| 6 | use serde::{Deserialize, Serialize}; | |
| 7 | ||
| 8 | use crate::User; | |
| 9 | ||
| 10 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 11 | #[serde(rename_all = "camelCase")] | |
| 12 | pub 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-a16631325aecf58ca | 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>, | |
| Webhooks: every event, to your own addresses, signed and retried | 22 | } |
| 23 | ||
| Fine-grained personal tokens, workspace token rules and approvals in identity | 24 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] |
| Webhooks: every event, to your own addresses, signed and retried | 25 | #[serde(rename_all = "camelCase")] |
| 26 | pub 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 managers | 36 | /// 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 step | 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>, | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 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. | |
| Fine-grained personal tokens, workspace token rules and approvals in identity | 49 | #[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 managers | 50 | pub permissions: std::collections::BTreeMap<String, String>, |
| Fine-grained personal tokens, workspace token rules and approvals in identity | 51 | /// 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 managers | 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. | |
| Fine-grained personal tokens, workspace token rules and approvals in identity | 72 | #[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 managers | 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. | |
| Fine-grained personal tokens, workspace token rules and approvals in identity | 79 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] |
| 80 | pub admin: bool, | |
| Merge main into Artifacts Phase 2 | 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, | |
| Webhooks: every event, to your own addresses, signed and retried | 85 | } |
| 86 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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`). | |
| Webhooks: every event, to your own addresses, signed and retried | 91 | /// Returns `Outcome<SignedIn>`. |
| 92 | #[derive(Debug, Serialize, Deserialize)] | |
| 93 | pub 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 look | 96 | /// Who is asking, such as the visitor's IP address, for rate limits. |
| 97 | #[serde(default)] | |
| 98 | pub client: Option<String>, | |
| Webhooks: every event, to your own addresses, signed and retried | 99 | } |
| 100 | ||
| 101 | #[derive(Debug, Serialize, Deserialize)] | |
| 102 | #[serde(rename_all = "camelCase")] | |
| 103 | pub struct SignedIn { | |
| 104 | pub user: User, | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 105 | /// Empty while `two_factor_challenge` is set: no session is made until |
| 106 | /// the code is given. | |
| Webhooks: every event, to your own addresses, signed and retried | 107 | pub session_token: String, |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 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>, | |
| Webhooks: every event, to your own addresses, signed and retried | 113 | } |
| 114 | ||
| 115 | /// `sign_out` and `user_for_session`. | |
| 116 | #[derive(Debug, Serialize, Deserialize)] | |
| 117 | #[serde(rename_all = "camelCase")] | |
| 118 | pub 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)] | |
| 124 | pub struct GitCredentialsArgs { | |
| 125 | pub username: String, | |
| 126 | pub secret: String, | |
| 127 | } | |
| 128 | ||
| 129 | /// `user_for_access_token`. | |
| 130 | #[derive(Debug, Serialize, Deserialize)] | |
| 131 | pub struct TokenArgs { | |
| 132 | pub token: String, | |
| 133 | } | |
| 134 | ||
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 135 | /// `user_for_ssh_key`, and `principal_for_ssh_key` (see [`crate::deploy_keys`]). |
| Webhooks: every event, to your own addresses, signed and retried | 136 | #[derive(Debug, Serialize, Deserialize)] |
| 137 | pub struct FingerprintArgs { | |
| 138 | pub fingerprint: String, | |
| 139 | } | |
| 140 | ||
| 141 | /// `user_by_username`. | |
| 142 | #[derive(Debug, Serialize, Deserialize)] | |
| 143 | pub 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-a69aeabc4b0deeb97 | 149 | /// not know are left out. Also `accounts`: the accounts behind user ids, |
| 150 | /// each with its username and avatar (`HashMap<String, accounts::EmailOwner>`). | |
| Webhooks: every event, to your own addresses, signed and retried | 151 | #[derive(Debug, Serialize, Deserialize)] |
| 152 | pub 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 calendar | 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)] | |
| 161 | pub struct DisplayUsernamesArgs { | |
| 162 | pub usernames: Vec<String>, | |
| 163 | } | |
| 164 | ||
| Webhooks: every event, to your own addresses, signed and retried | 165 | /// `list_ssh_keys` and `list_access_tokens`. |
| 166 | #[derive(Debug, Serialize, Deserialize)] | |
| 167 | pub struct UserArgs { | |
| 168 | pub user: User, | |
| 169 | } | |
| 170 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 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)] | |
| 176 | pub struct SshKeyOwnersArgs { | |
| 177 | pub fingerprints: Vec<String>, | |
| 178 | } | |
| 179 | ||
| Webhooks: every event, to your own addresses, signed and retried | 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")] | |
| 184 | pub 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)] | |
| 192 | pub 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")] | |
| 202 | pub 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 step | 206 | /// left out of the user's token list, unless `listed`. Used for hosted |
| 207 | /// attempts. | |
| Webhooks: every event, to your own addresses, signed and retried | 208 | #[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 step | 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 | ||
| 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")] | |
| 227 | pub 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")] | |
| 247 | pub struct RevokeJobTokensArgs { | |
| 248 | pub job_id: String, | |
| 249 | } | |
| 250 | ||
| Webhooks: every event, to your own addresses, signed and retried | 251 | /// The plaintext token is returned once and never stored. |
| 252 | #[derive(Debug, Serialize, Deserialize)] | |
| 253 | pub 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 look | 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`]. | |
| Webhooks: every event, to your own addresses, signed and retried | 264 | #[derive(Debug, Serialize, Deserialize)] |
| 265 | pub 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 look | 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>, | |
| 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 confirm | 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>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 278 | /// Who is asking, such as the visitor's IP address, for rate limits. |
| 279 | #[serde(default)] | |
| 280 | pub client: Option<String>, | |
| Webhooks: every event, to your own addresses, signed and retried | 281 | } |
| 282 | ||
| 283 | /// `verify_email`: the token from the emailed link. Returns `Outcome<User>`. | |
| 284 | #[derive(Debug, Serialize, Deserialize)] | |
| 285 | pub 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 look | 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. | |
| Webhooks: every event, to your own addresses, signed and retried | 294 | #[derive(Debug, Serialize, Deserialize)] |
| 295 | pub struct EmailArgs { | |
| 296 | pub email: String, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 297 | /// Who is asking, such as the visitor's IP address, for rate limits. |
| 298 | #[serde(default)] | |
| 299 | pub client: Option<String>, | |
| Webhooks: every event, to your own addresses, signed and retried | 300 | } |
| 301 | ||
| 302 | /// `reset_password`: sets a new password and ends every session. | |
| 303 | /// Returns `Outcome<User>`. | |
| 304 | #[derive(Debug, Serialize, Deserialize)] | |
| 305 | pub 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")] | |
| 313 | pub 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")] | |
| 320 | pub 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")] | |
| 335 | pub struct DeviceLookupArgs { | |
| 336 | pub user_code: String, | |
| 337 | } | |
| 338 | ||
| 339 | #[derive(Debug, Serialize, Deserialize)] | |
| 340 | #[serde(rename_all = "camelCase")] | |
| 341 | pub 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")] | |
| 350 | pub 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")] | |
| 359 | pub 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")] | |
| 366 | pub 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")] | |
| 383 | pub 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 control | 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>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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, | |
| 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 tokens | 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, | |
| Webhooks: every event, to your own addresses, signed and retried | 412 | } |
| 413 | ||
| 414 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 415 | pub struct Member { | |
| 416 | pub username: String, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 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>, | |
| Webhooks: every event, to your own addresses, signed and retried | 421 | pub role: crate::Role, |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 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>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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>, | |
| Webhooks: every event, to your own addresses, signed and retried | 436 | } |
| 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")] | |
| 444 | pub enum DataResidency { | |
| 445 | #[default] | |
| 446 | Anywhere, | |
| 447 | Eu, | |
| 448 | } | |
| 449 | ||
| 450 | impl 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)] | |
| 471 | pub struct SetResidencyArgs { | |
| 472 | pub actor: User, | |
| 473 | pub slug: String, | |
| 474 | pub residency: DataResidency, | |
| 475 | } | |
| 476 | ||
| Webhooks: every event, to your own addresses, signed and retried | 477 | /// `create_workspace`. Returns `Outcome<Workspace>`. |
| 478 | #[derive(Debug, Serialize, Deserialize)] | |
| 479 | pub 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)] | |
| 488 | pub struct SlugArgs { | |
| 489 | pub slug: String, | |
| 490 | } | |
| 491 | ||
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 492 | /// `list_members`: members only. Owners also see each member's |
| 493 | /// `two_factor`. Returns `Outcome<Vec<Member>>`. | |
| Webhooks: every event, to your own addresses, signed and retried | 494 | #[derive(Debug, Serialize, Deserialize)] |
| 495 | pub 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 tokens | 504 | /// leaving (`members::LeaveWorkspaceArgs`); removing an owner is refused |
| 505 | /// when they are the last. Each returns `Outcome<bool>`. | |
| Webhooks: every event, to your own addresses, signed and retried | 506 | #[derive(Debug, Serialize, Deserialize)] |
| 507 | pub struct MemberArgs { | |
| 508 | pub actor: User, | |
| 509 | pub slug: String, | |
| 510 | pub username: String, | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 511 | #[serde(default)] |
| 512 | pub surface: Option<crate::audit::Surface>, | |
| Webhooks: every event, to your own addresses, signed and retried | 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)] | |
| 518 | pub 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 domains | 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")] | |
| 535 | pub 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 look | 541 | /// `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 member | 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>`. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 553 | /// |
| 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 member | 555 | /// 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 look | 557 | #[derive(Debug, Serialize, Deserialize)] |
| 558 | pub 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 member | 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`. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 570 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 571 | pub struct WorkspaceDeletion { | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 572 | /// Its live repositories, which are deleted with it. |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 573 | pub repositories: u32, |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 574 | /// Its projects, hidden with it. |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 575 | pub projects: u32, |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 576 | #[serde(default)] |
| 577 | pub members: u32, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 578 | /// 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 member | 580 | /// 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 look | 583 | } |
| 584 | ||
| 585 | impl 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 member | 587 | self.protected || self.billing.is_some() |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 588 | } |
| 589 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 590 | /// 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 look | 591 | 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 member | 592 | if self.protected { |
| 593 | return Some(protected_refusal(slug)); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 594 | } |
| 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 member | 599 | /// How long a deleted workspace is kept, for staff to restore, before it is |
| 600 | /// purged. | |
| 601 | pub const WORKSPACE_RESTORE_DAYS: u64 = 30; | |
| 602 | ||
| 603 | /// Workspaces nobody can delete, whatever identity's `PROTECTED_WORKSPACES` | |
| 604 | /// says: Flagon's, which runs g1t. | |
| 605 | pub 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. | |
| 611 | pub 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. | |
| 624 | pub 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")] | |
| 635 | pub 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 each | 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. | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 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")] | |
| 660 | pub 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 look | 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)] | |
| 671 | pub 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 domains | 676 | /// How long a workspace's old slug keeps redirecting to it, and stays |
| 677 | /// reserved for it, after a rename. | |
| 678 | pub const SLUG_HOLD_DAYS: u64 = 90; | |
| 679 | ||
| 680 | /// How long a workspace must wait between renames. | |
| 681 | pub 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")] | |
| 700 | pub 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")] | |
| 722 | pub 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")] | |
| 734 | pub 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 domains | 739 | |
| Workspace names and icons, and a component kit for every control | 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)] | |
| 744 | pub 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)] | |
| 753 | pub struct SetUserAvatarArgs { | |
| 754 | pub user: User, | |
| 755 | pub image: Option<String>, | |
| 756 | } | |
| 757 | ||
| 758 | /// The largest avatar that can be uploaded, in bytes. | |
| 759 | pub const MAX_AVATAR_BYTES: usize = 1024 * 1024; | |
| 760 | ||
| Webhooks: every event, to your own addresses, signed and retried | 761 | /// `list_workspace_tokens`: members only. Returns |
| 762 | /// `Outcome<Vec<AccessToken>>`. | |
| 763 | #[derive(Debug, Serialize, Deserialize)] | |
| 764 | pub 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)] | |
| 771 | pub 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")] | |
| 782 | pub 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 step | 790 | /// What the person granted, as `resource:level`. Null: full access. |
| 791 | #[serde(default)] | |
| 792 | pub scopes: Option<Vec<String>>, | |
| Webhooks: every event, to your own addresses, signed and retried | 793 | } |
| 794 | ||
| 795 | #[derive(Debug, Serialize, Deserialize)] | |
| 796 | pub 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")] | |
| 804 | pub 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")] | |
| 815 | pub struct OAuthRefreshArgs { | |
| 816 | pub refresh_token: String, | |
| 817 | pub client_id: String, | |
| 818 | } | |
| 819 | ||
| 820 | #[derive(Debug, Serialize, Deserialize)] | |
| 821 | #[serde(rename_all = "camelCase")] | |
| 822 | pub 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 step | 828 | /// The scopes granted, space-separated, or `*` for full access. |
| 829 | #[serde(default)] | |
| 830 | pub scope: Option<String>, | |
| Webhooks: every event, to your own addresses, signed and retried | 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")] | |
| 837 | pub 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 step | 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)] | |
| 856 | pub struct UpdateOAuthGrantArgs { | |
| 857 | pub user: User, | |
| 858 | pub id: String, | |
| 859 | #[serde(default)] | |
| 860 | pub scopes: Option<Vec<String>>, | |
| Webhooks: every event, to your own addresses, signed and retried | 861 | } |
| 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 API | 865 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| Webhooks: every event, to your own addresses, signed and retried | 866 | pub 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 API | 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>, | |
| Webhooks: every event, to your own addresses, signed and retried | 874 | } |
| 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-agent | 877 | /// behalf. It acts as `g1t`, a member of the repository's workspace, |
| Webhooks: every event, to your own addresses, signed and retried | 878 | /// and only for the operations in `scope`. Returns `CreatedAccessToken`. |
| 879 | #[derive(Debug, Serialize, Deserialize)] | |
| 880 | #[serde(rename_all = "camelCase")] | |
| 881 | pub 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-agent | 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. | |
| Webhooks: every event, to your own addresses, signed and retried | 894 | pub 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-agent | 895 | /// The name g1t's agent is shown by: g1t's own, [`crate::system::USERNAME`]. |
| 896 | /// Everything it does, people see g1t do. | |
| 897 | pub const AGENT_NAME: &str = crate::system::USERNAME; | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 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)] | |
| 911 | pub 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)] | |
| 929 | pub struct AdminWorkspacesArgs { | |
| 930 | #[serde(default)] | |
| 931 | pub query: Option<String>, | |
| 932 | } | |
| 933 | ||
| 934 | /// The most workspaces one `admin_workspaces` call returns. | |
| 935 | pub const ADMIN_WORKSPACES_LIMIT: usize = 500; | |
| 936 | ||
| 937 | /// An owner of a workspace, as staff see them. | |
| 938 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 939 | pub 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")] | |
| 947 | pub 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")] | |
| 960 | pub 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 member | 968 | /// 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 workspace | 971 | } |
| 972 | ||
| 973 | /// A member of a workspace, as staff see them. | |
| 974 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 975 | pub 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 domains | 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. | |
| 989 | pub const MAX_PROFILE_NAME: usize = 80; | |
| 990 | pub const MAX_PROFILE_BIO: usize = 160; | |
| 991 | pub const MAX_PROFILE_LOCATION: usize = 80; | |
| 992 | pub const MAX_PROFILE_WEBSITE: usize = 200; | |
| 993 | pub 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) | 994 | pub const MAX_PROFILE_TIMEZONE: usize = 64; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 995 | |
| 996 | /// What anyone may see about a person. | |
| 997 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 998 | #[serde(rename_all = "camelCase")] | |
| 999 | pub 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 managers | 1000 | /// Lowercased: what the profile is found and linked by. |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 1001 | 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 managers | 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>, | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 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>, | |
| 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 domains | 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")] | |
| 1029 | pub 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 domains | 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)] | |
| 1053 | pub 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)] | |
| 1062 | pub 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 palette | 1067 | |
| 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. | |
| 1075 | pub const MAX_DOCK_PINS: usize = 24; | |
| 1076 | /// The most characters an app key takes. | |
| 1077 | pub 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")] | |
| 1084 | pub 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")] | |
| 1096 | pub struct SetDockPinsArgs { | |
| 1097 | pub user: User, | |
| 1098 | pub workspace: String, | |
| 1099 | #[serde(default)] | |
| 1100 | pub apps: Vec<String>, | |
| 1101 | } | |
| 1102 | ||
| Search across all of g1t, Explore, and a command palette | 1103 | /// `directory`: every account or every workspace, as their public pages |
| 1104 | /// show them, a page at a time in name order. For services that index | |
| 1105 | /// them, such as search; nothing private is in it. Returns | |
| 1106 | /// `DirectoryPage`. | |
| 1107 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 1108 | pub struct DirectoryArgs { | |
| 1109 | /// `user` or `workspace`. | |
| 1110 | pub kind: String, | |
| 1111 | /// Names after this one. | |
| 1112 | #[serde(default)] | |
| 1113 | pub after: Option<String>, | |
| 1114 | pub limit: u32, | |
| 1115 | } | |
| 1116 | ||
| 1117 | /// One account or workspace in the directory. | |
| 1118 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1119 | #[serde(rename_all = "camelCase")] | |
| 1120 | pub struct DirectoryEntry { | |
| 1121 | /// The account's or workspace's id. | |
| 1122 | pub id: String, | |
| 1123 | /// A username or a workspace's slug. | |
| 1124 | pub slug: String, | |
| 1125 | /// A person's display name or a workspace's name. | |
| 1126 | pub name: Option<String>, | |
| 1127 | /// A person's bio or a workspace's description. | |
| 1128 | pub bio: Option<String>, | |
| 1129 | pub avatar: Option<String>, | |
| 1130 | /// RFC 3339. | |
| 1131 | pub created_at: String, | |
| 1132 | } | |
| 1133 | ||
| 1134 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 1135 | pub struct DirectoryPage { | |
| 1136 | pub entries: Vec<DirectoryEntry>, | |
| 1137 | /// Where the next page starts; null on the last. | |
| 1138 | pub next: Option<String>, | |
| 1139 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1140 | |
| 1141 | // --- Invites --------------------------------------------------------------- | |
| 1142 | // | |
| 1143 | // While registration is invite-only, every new account (with a password or | |
| 1144 | // through GitHub) needs an invite code. Each person may have | |
| 1145 | // `INVITES_PER_USER` invites out at a time; staff grant more to a person or | |
| 1146 | // to a workspace, whose owners share them. Inviting an email with no | |
| 1147 | // account into a workspace makes an invite bound to that address, which | |
| 1148 | // registers and joins in one step. See services/identity/src/invites.rs. | |
| 1149 | ||
| 1150 | /// Whether anyone may make an account, or only someone with an invite. Set | |
| 1151 | /// by identity's `REGISTRATION_MODE` var; anything but `open`, including | |
| 1152 | /// leaving it unset, is `invite`, so a missing setting never opens sign-up. | |
| 1153 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 1154 | #[serde(rename_all = "snake_case")] | |
| 1155 | pub enum RegistrationMode { | |
| 1156 | #[default] | |
| 1157 | Invite, | |
| 1158 | Open, | |
| 1159 | } | |
| 1160 | ||
| 1161 | impl RegistrationMode { | |
| 1162 | pub fn parse(text: Option<&str>) -> RegistrationMode { | |
| 1163 | match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() { | |
| 1164 | Some("open") => RegistrationMode::Open, | |
| 1165 | _ => RegistrationMode::Invite, | |
| 1166 | } | |
| 1167 | } | |
| 1168 | } | |
| 1169 | ||
| 1170 | /// How many invites a person may have out at once, unless identity's | |
| 1171 | /// `INVITES_PER_USER` var says otherwise. | |
| 1172 | pub const INVITES_PER_USER: u32 = 5; | |
| 1173 | ||
| 1174 | /// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says | |
| 1175 | /// otherwise. | |
| 1176 | pub const INVITE_TTL_DAYS: u64 = 30; | |
| 1177 | ||
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 1178 | /// Where an invite stands. Only a pending invite can be used. A pending |
| 1179 | /// invite can be revoked, and so can one awaiting confirmation. An expired | |
| 1180 | /// or revoked invite that was never used gives its inviter the invite back. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1181 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 1182 | #[serde(rename_all = "snake_case")] | |
| 1183 | pub enum InviteStatus { | |
| 1184 | Pending, | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 1185 | /// Used to make an account that has not confirmed its email address |
| 1186 | /// yet. The code is spent; the workspace (or repository) it gives is | |
| 1187 | /// joined when the address is confirmed, unless it is revoked first. | |
| 1188 | AwaitingConfirmation, | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1189 | /// Used to make an account that has confirmed its address, for a |
| 1190 | /// workspace it has not yet joined or declined: the workspace | |
| 1191 | /// invitation waits for the person's answer (`accept_invitation`). | |
| 1192 | AwaitingAnswer, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1193 | Redeemed, |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1194 | /// Its person declined the workspace it invited them to. |
| 1195 | Declined, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1196 | Expired, |
| 1197 | Revoked, | |
| 1198 | } | |
| 1199 | ||
| 1200 | /// What using an invite does. | |
| 1201 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1202 | #[serde(rename_all = "snake_case")] | |
| 1203 | pub enum InviteKind { | |
| 1204 | /// Makes a new account, and joins `workspace` when one is set. | |
| 1205 | Account, | |
| 1206 | /// An existing account joins `workspace`. Never makes an account. | |
| 1207 | Workspace, | |
| 1208 | } | |
| 1209 | ||
| 1210 | /// Whose allowance an invite uses. | |
| 1211 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1212 | #[serde(rename_all = "snake_case")] | |
| 1213 | pub enum InviteCharge { | |
| 1214 | /// Its inviter's own. | |
| 1215 | User, | |
| 1216 | /// The workspace's, granted by staff and shared by its owners. | |
| 1217 | Workspace, | |
| 1218 | /// Nobody's: staff minted it, or it invites an existing account. | |
| 1219 | None, | |
| 1220 | } | |
| 1221 | ||
| 1222 | /// One invite, as the person who made it sees it. | |
| 1223 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1224 | #[serde(rename_all = "camelCase")] | |
| 1225 | pub struct Invite { | |
| 1226 | pub id: String, | |
| 1227 | /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite | |
| 1228 | /// is made, and afterwards to whoever made it while it is pending. | |
| 1229 | /// Null otherwise. | |
| 1230 | pub code: Option<String>, | |
| 1231 | /// The code's first group, such as `g1t-k7m2`, to recognise it by. | |
| 1232 | pub hint: String, | |
| 1233 | /// Only an account with this address can use it. Null: anyone with | |
| 1234 | /// the code. | |
| 1235 | pub email: Option<String>, | |
| 1236 | pub kind: InviteKind, | |
| 1237 | /// The workspace it joins, by slug. | |
| 1238 | pub workspace: Option<String>, | |
| 1239 | pub status: InviteStatus, | |
| 1240 | pub charged_to: InviteCharge, | |
| 1241 | /// Who made it, by username. Null when g1t staff did. | |
| 1242 | pub invited_by: Option<String>, | |
| 1243 | /// The account that used it, by username. | |
| 1244 | pub redeemed_by: Option<String>, | |
| 1245 | /// RFC 3339. | |
| 1246 | pub created_at: String, | |
| 1247 | /// RFC 3339. | |
| 1248 | pub expires_at: String, | |
| 1249 | /// RFC 3339. | |
| 1250 | pub redeemed_at: Option<String>, | |
| 1251 | /// RFC 3339. | |
| 1252 | pub revoked_at: Option<String>, | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1253 | /// The account a workspace invitation is for, by username: someone |
| 1254 | /// invited by username, or the account the invite made. | |
| 1255 | #[serde(default)] | |
| 1256 | pub invitee: Option<String>, | |
| 1257 | /// The role `workspace` is joined with. Null when it names none. | |
| 1258 | #[serde(default)] | |
| 1259 | pub role: Option<crate::Role>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1260 | /// The staff member who minted it. Only in staff views. |
| 1261 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 1262 | pub staff: Option<String>, | |
| 1263 | } | |
| 1264 | ||
| 1265 | /// How many invites someone may have out, and how many they have. | |
| 1266 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 1267 | pub struct Allowance { | |
| 1268 | /// Null: no limit. | |
| 1269 | pub limit: Option<u32>, | |
| 1270 | /// Pending and used invites; revoked and expired ones are not counted. | |
| 1271 | pub used: u32, | |
| 1272 | /// Null: no limit. | |
| 1273 | pub remaining: Option<u32>, | |
| 1274 | } | |
| 1275 | ||
| 1276 | impl Allowance { | |
| 1277 | pub fn new(limit: Option<u32>, used: u32) -> Allowance { | |
| 1278 | Allowance { | |
| 1279 | limit, | |
| 1280 | used, | |
| 1281 | remaining: limit.map(|limit| limit.saturating_sub(used)), | |
| 1282 | } | |
| 1283 | } | |
| 1284 | ||
| 1285 | pub fn exhausted(&self) -> bool { | |
| 1286 | self.remaining == Some(0) | |
| 1287 | } | |
| 1288 | } | |
| 1289 | ||
| 1290 | /// A workspace's shared invites, for one of its owners. | |
| 1291 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1292 | pub struct WorkspaceAllowance { | |
| 1293 | pub slug: String, | |
| 1294 | pub allowance: Allowance, | |
| 1295 | } | |
| 1296 | ||
| 1297 | /// `list_invites` (takes `UserArgs`): a person's invites, newest first, | |
| 1298 | /// and what they have left. Returns `InvitesOverview`. | |
| 1299 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1300 | pub struct InvitesOverview { | |
| 1301 | pub mode: RegistrationMode, | |
| 1302 | pub allowance: Allowance, | |
| 1303 | /// Workspaces the person owns that staff granted invites to. | |
| 1304 | pub workspaces: Vec<WorkspaceAllowance>, | |
| 1305 | pub invites: Vec<Invite>, | |
| 1306 | } | |
| 1307 | ||
| 1308 | /// `create_invite`: a person makes an invite, optionally for one email | |
| 1309 | /// address. People only; never an agent or a workspace's token, and not | |
| 1310 | /// before their email is confirmed. Uses one of the person's invites, or, | |
| 1311 | /// with `workspace`, one of the invites staff granted that workspace (its | |
| 1312 | /// owners only). Emails the address when one is given. Returns | |
| 1313 | /// `Outcome<Invite>`, with the code. | |
| 1314 | /// | |
| 1315 | /// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending | |
| 1316 | /// invite; a workspace's owners may revoke one made for the workspace. | |
| 1317 | /// The invite comes back to whoever it was charged to. Returns | |
| 1318 | /// `Outcome<Invite>`. | |
| 1319 | #[derive(Debug, Serialize, Deserialize)] | |
| 1320 | pub struct CreateInviteArgs { | |
| 1321 | pub user: User, | |
| 1322 | #[serde(default)] | |
| 1323 | pub email: Option<String>, | |
| 1324 | /// Use this workspace's granted invites, by slug. | |
| 1325 | #[serde(default)] | |
| 1326 | pub workspace: Option<String>, | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1327 | /// The workspace the new account is invited to, by slug: one the |
| 1328 | /// person owns that can add members (not on the free plan). Once the | |
| 1329 | /// account is confirmed it gets a workspace invitation to accept, as a | |
| 1330 | /// member, and no workspace of its own is made for it. | |
| 1331 | #[serde(default)] | |
| 1332 | pub join: Option<String>, | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 1333 | /// The role `join` invites them with; member when absent. Ignored |
| 1334 | /// without `join`. | |
| 1335 | #[serde(default)] | |
| 1336 | pub join_role: Option<crate::Role>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1337 | /// Where the request came in, for the audit log; g1t.sh when absent. |
| 1338 | #[serde(default)] | |
| 1339 | pub surface: Option<crate::audit::Surface>, | |
| 1340 | } | |
| 1341 | ||
| 1342 | /// `check_invite`: what an invite code is for, before using it. Returns | |
| 1343 | /// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1344 | /// expired gets the same answer, so codes cannot be probed. With |
| 1345 | /// `any_status`, a real code that can no longer be used is described | |
| 1346 | /// instead (its `status` says why), so the page can say whom to ask for a | |
| 1347 | /// new one; an unknown code still gets the one answer. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1348 | #[derive(Debug, Serialize, Deserialize)] |
| 1349 | pub struct InviteCodeArgs { | |
| 1350 | pub code: String, | |
| 1351 | /// Who is asking, such as the visitor's IP address, for rate limits. | |
| 1352 | #[serde(default)] | |
| 1353 | pub client: Option<String>, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1354 | /// Who is looking, if signed in: sets `InvitePreview::for_viewer`. |
| 1355 | #[serde(default)] | |
| 1356 | pub viewer: Option<User>, | |
| 1357 | #[serde(default)] | |
| 1358 | pub any_status: bool, | |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 1359 | /// The `proof` from the invite email's link, if the page was opened |
| 1360 | /// from it: sets `InvitePreview::email_proven`. | |
| 1361 | #[serde(default)] | |
| 1362 | pub email_proof: Option<String>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1363 | } |
| 1364 | ||
| 1365 | /// Someone shown on an invite. | |
| 1366 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1367 | pub struct InviteFrom { | |
| 1368 | pub username: String, | |
| 1369 | pub name: Option<String>, | |
| 1370 | pub avatar: Option<String>, | |
| 1371 | } | |
| 1372 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1373 | /// A repository an invite code was sent with: using the code accepts the |
| 1374 | /// invitation to collaborate on it. | |
| 1375 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1376 | pub struct InviteRepository { | |
| 1377 | /// `workspace/repo`. | |
| 1378 | pub name: String, | |
| 1379 | /// The role it gives, such as `write`. | |
| 1380 | pub role: String, | |
| 1381 | } | |
| 1382 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1383 | /// What a valid invite code is for. |
| 1384 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1385 | #[serde(rename_all = "camelCase")] | |
| 1386 | pub struct InvitePreview { | |
| 1387 | pub kind: InviteKind, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1388 | /// Pending, unless `any_status` asked about a code that is spent. |
| 1389 | pub status: InviteStatus, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1390 | /// Null when g1t staff sent it. |
| 1391 | pub invited_by: Option<InviteFrom>, | |
| 1392 | pub workspace: Option<ProfileWorkspace>, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1393 | /// The repository it accepts an invitation to, if it was sent with one. |
| 1394 | pub repository: Option<InviteRepository>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1395 | /// The address it is for, partly hidden, such as `a•••@example.com`. |
| 1396 | pub email: Option<String>, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1397 | /// The address in full, while it is pending: whoever holds the code |
| 1398 | /// was sent it there. Fills in and locks the sign-up form. | |
| 1399 | pub address: Option<String>, | |
| 1400 | /// Whether the address it is for has a g1t account already, so the | |
| 1401 | /// page asks them to sign in rather than sign up. | |
| 1402 | pub has_account: bool, | |
| 1403 | /// With a viewer: whether the invite is theirs (it is for one of their | |
| 1404 | /// confirmed addresses, or they used it). Null without a viewer or, | |
| 1405 | /// for a pending invite, when it is for anyone with the code. | |
| 1406 | pub for_viewer: Option<bool>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1407 | /// RFC 3339. |
| 1408 | pub expires_at: String, | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 1409 | /// For a shared invite link ([`SharedInvite`]): the group it was made |
| 1410 | /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows | |
| 1411 | /// it. Null for a one-person invite. | |
| 1412 | #[serde(default)] | |
| 1413 | pub shared_label: Option<String>, | |
| 1414 | /// For a shared invite link limited to some email domains: those | |
| 1415 | /// domains, such as `["cloudflare.com"]`. Empty for any address. | |
| 1416 | #[serde(default)] | |
| 1417 | pub shared_domains: Vec<String>, | |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 1418 | /// Whether `email_proof` was this pending invite's own, from the email |
| 1419 | /// it was sent in: the account made with it starts with `address` | |
| 1420 | /// confirmed. False without a proof, with a wrong one, and for an | |
| 1421 | /// invite bound to no address. | |
| 1422 | #[serde(default)] | |
| 1423 | pub email_proven: bool, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1424 | } |
| 1425 | ||
| 1426 | /// `accept_invite`: a signed-in person uses a workspace invite made for | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1427 | /// their confirmed address, and joins the workspace, or an invite sent with |
| 1428 | /// a repository invitation, and accepts it. Returns `Outcome<String>`: the | |
| 1429 | /// workspace's slug, or `workspace/repo`. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1430 | #[derive(Debug, Serialize, Deserialize)] |
| 1431 | pub struct AcceptInviteArgs { | |
| 1432 | pub user: User, | |
| 1433 | pub code: String, | |
| 1434 | } | |
| 1435 | ||
| 1436 | /// `invite_member`: an owner invites an email address into a workspace. | |
| 1437 | /// It always makes an invite bound to that address and emails it, so the | |
| 1438 | /// answer never says whether the address has an account. Without one, the | |
| 1439 | /// invite registers and joins in one step, and uses one of the workspace's | |
| 1440 | /// granted invites or else one of the owner's own. With one, it costs | |
| 1441 | /// nothing. Returns `Outcome<Invite>`, with the code. | |
| 1442 | #[derive(Debug, Serialize, Deserialize)] | |
| 1443 | pub struct InviteMemberArgs { | |
| 1444 | pub actor: User, | |
| 1445 | pub slug: String, | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1446 | /// An email address. Give this or `username`. |
| 1447 | #[serde(default)] | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1448 | pub email: String, |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1449 | /// A g1t username: that account gets a workspace invitation to accept |
| 1450 | /// or decline, in its inbox and by email. Nobody joins without saying | |
| 1451 | /// yes. | |
| 1452 | #[serde(default)] | |
| 1453 | pub username: Option<String>, | |
| 1454 | /// The role they join with; member when absent. | |
| 1455 | #[serde(default)] | |
| 1456 | pub role: Option<crate::Role>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1457 | /// Where the request came in, for the audit log; g1t.sh when absent. |
| 1458 | #[serde(default)] | |
| 1459 | pub surface: Option<crate::audit::Surface>, | |
| 1460 | } | |
| 1461 | ||
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 1462 | /// A workspace invitation waiting for its person's answer, as they see it. |
| 1463 | /// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`, | |
| 1464 | /// newest first: pending ones only, never expired, revoked or answered. | |
| 1465 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1466 | #[serde(rename_all = "camelCase")] | |
| 1467 | pub struct WorkspaceInvitation { | |
| 1468 | pub id: String, | |
| 1469 | pub workspace: ProfileWorkspace, | |
| 1470 | /// The role accepting joins with. | |
| 1471 | pub role: crate::Role, | |
| 1472 | /// Null when g1t staff sent it. | |
| 1473 | pub invited_by: Option<InviteFrom>, | |
| 1474 | /// RFC 3339. | |
| 1475 | pub created_at: String, | |
| 1476 | /// RFC 3339. | |
| 1477 | pub expires_at: String, | |
| 1478 | } | |
| 1479 | ||
| 1480 | /// `accept_invitation`: the person it is for joins the workspace with the | |
| 1481 | /// role it names. Returns `Outcome<String>`, the workspace's slug. | |
| 1482 | /// | |
| 1483 | /// `decline_invitation`: they say no; whoever sent it is told in their | |
| 1484 | /// inbox. Returns `Outcome<bool>`. | |
| 1485 | #[derive(Debug, Serialize, Deserialize)] | |
| 1486 | pub struct InvitationArgs { | |
| 1487 | pub user: User, | |
| 1488 | pub id: String, | |
| 1489 | /// Where the request came in, for the audit log; g1t.sh when absent. | |
| 1490 | #[serde(default)] | |
| 1491 | pub surface: Option<crate::audit::Surface>, | |
| 1492 | } | |
| 1493 | ||
| 1494 | /// `find_people`: accounts whose username starts with `query`, or whose | |
| 1495 | /// name contains it, for picking someone to invite. Only what a profile | |
| 1496 | /// shows: a username, a name and an avatar, never an email address. | |
| 1497 | /// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent). | |
| 1498 | #[derive(Debug, Serialize, Deserialize)] | |
| 1499 | pub struct FindPeopleArgs { | |
| 1500 | pub query: String, | |
| 1501 | #[serde(default)] | |
| 1502 | pub limit: Option<u32>, | |
| 1503 | } | |
| 1504 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1505 | /// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites, |
| 1506 | /// newest first. Owners only. Returns `Outcome<Vec<Invite>>`. | |
| 1507 | /// | |
| 1508 | /// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`. | |
| 1509 | #[derive(Debug, Serialize, Deserialize)] | |
| 1510 | pub struct WorkspaceInviteArgs { | |
| 1511 | pub actor: User, | |
| 1512 | pub slug: String, | |
| 1513 | pub id: String, | |
| 1514 | } | |
| 1515 | ||
| 1516 | /// `request_access`: someone without an invite asks for one. Kept on the | |
| 1517 | /// waitlist, one entry per address. Answers the same way whether or not | |
| 1518 | /// the address is already on it. Returns `Outcome<bool>`. | |
| 1519 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 1520 | pub struct RequestAccessArgs { | |
| 1521 | pub email: String, | |
| 1522 | /// What they will build, if they said. | |
| 1523 | #[serde(default)] | |
| 1524 | pub about: String, | |
| 1525 | /// Who is asking, such as the visitor's IP address, for rate limits. | |
| 1526 | #[serde(default)] | |
| 1527 | pub client: Option<String>, | |
| 1528 | } | |
| 1529 | ||
| 1530 | /// The most characters `RequestAccessArgs::about` keeps. | |
| 1531 | pub const MAX_WAITLIST_ABOUT: usize = 1000; | |
| 1532 | ||
| 1533 | // `registration` takes `{}` and returns `RegistrationMode`. | |
| 1534 | ||
| 1535 | // --- Invites, staff only --- | |
| 1536 | ||
| 1537 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1538 | #[serde(rename_all = "snake_case")] | |
| 1539 | pub enum WaitlistStatus { | |
| 1540 | Waiting, | |
| 1541 | Invited, | |
| 1542 | Dismissed, | |
| 1543 | } | |
| 1544 | ||
| 1545 | impl WaitlistStatus { | |
| 1546 | pub fn as_str(self) -> &'static str { | |
| 1547 | match self { | |
| 1548 | WaitlistStatus::Waiting => "waiting", | |
| 1549 | WaitlistStatus::Invited => "invited", | |
| 1550 | WaitlistStatus::Dismissed => "dismissed", | |
| 1551 | } | |
| 1552 | } | |
| 1553 | } | |
| 1554 | ||
| 1555 | /// Someone who asked for access. | |
| 1556 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1557 | #[serde(rename_all = "camelCase")] | |
| 1558 | pub struct WaitlistEntry { | |
| 1559 | pub id: String, | |
| 1560 | pub email: String, | |
| 1561 | pub about: Option<String>, | |
| 1562 | pub status: WaitlistStatus, | |
| 1563 | pub invite_id: Option<String>, | |
| 1564 | pub decided_by: Option<String>, | |
| 1565 | /// RFC 3339. | |
| 1566 | pub decided_at: Option<String>, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1567 | /// What staff wrote when approving; it went in the invite email. |
| 1568 | #[serde(default)] | |
| 1569 | pub note: Option<String>, | |
| 1570 | /// The account made with the invite, once it was used. | |
| 1571 | #[serde(default)] | |
| 1572 | pub joined_as: Option<String>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1573 | /// When they first asked. RFC 3339. |
| 1574 | pub created_at: String, | |
| 1575 | /// When they last asked. RFC 3339. | |
| 1576 | pub updated_at: String, | |
| 1577 | } | |
| 1578 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1579 | /// `admin_waitlist`: the waitlist, newest first, at most |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1580 | /// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`. |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1581 | /// |
| 1582 | /// `admin_waitlist_pending` takes `{}` and returns the number of requests | |
| 1583 | /// still waiting, for sudo's navigation. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1584 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 1585 | pub struct AdminWaitlistArgs { | |
| 1586 | /// Part of an email address or of what they said. | |
| 1587 | #[serde(default)] | |
| 1588 | pub query: Option<String>, | |
| 1589 | /// Null: every status. | |
| 1590 | #[serde(default)] | |
| 1591 | pub status: Option<WaitlistStatus>, | |
| 1592 | } | |
| 1593 | ||
| 1594 | /// The most rows one staff listing of invites or the waitlist returns. | |
| 1595 | pub const ADMIN_INVITES_LIMIT: usize = 500; | |
| 1596 | ||
| 1597 | /// `admin_decide_waitlist`: approving mints an invite bound to the | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1598 | /// address, charged to nobody, and emails it, with `note` if given; |
| 1599 | /// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1600 | #[derive(Debug, Serialize, Deserialize)] |
| 1601 | pub struct AdminDecideWaitlistArgs { | |
| 1602 | pub id: String, | |
| 1603 | pub approve: bool, | |
| 1604 | /// The staff member, by email. | |
| 1605 | pub staff: String, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1606 | /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters. |
| 1607 | #[serde(default)] | |
| 1608 | pub note: Option<String>, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1609 | } |
| 1610 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1611 | /// The most characters an approval's note keeps. |
| 1612 | pub const MAX_WAITLIST_NOTE: usize = 500; | |
| 1613 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1614 | /// `admin_invites`: every invite, newest first, at most |
| 1615 | /// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with | |
| 1616 | /// `query`, or whose email, inviter or redeemer contains it. Returns | |
| 1617 | /// `Vec<Invite>`. | |
| 1618 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 1619 | pub struct AdminInvitesArgs { | |
| 1620 | #[serde(default)] | |
| 1621 | pub query: Option<String>, | |
| 1622 | } | |
| 1623 | ||
| 1624 | /// `admin_revoke_invite`: revokes any pending invite. Returns | |
| 1625 | /// `Outcome<Invite>`. | |
| 1626 | #[derive(Debug, Serialize, Deserialize)] | |
| 1627 | pub struct AdminRevokeInviteArgs { | |
| 1628 | pub id: String, | |
| 1629 | pub staff: String, | |
| 1630 | } | |
| 1631 | ||
| 1632 | /// `admin_mint_invite`: staff make an invite that uses nobody's | |
| 1633 | /// allowance, optionally bound to (and emailed to) an address. Returns | |
| 1634 | /// `Outcome<Invite>`, with the code. | |
| 1635 | #[derive(Debug, Serialize, Deserialize)] | |
| 1636 | pub struct AdminMintInviteArgs { | |
| 1637 | #[serde(default)] | |
| 1638 | pub email: Option<String>, | |
| 1639 | pub staff: String, | |
| 1640 | } | |
| 1641 | ||
| 1642 | /// Who staff grant invites to. | |
| 1643 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1644 | #[serde(rename_all = "snake_case")] | |
| 1645 | pub enum GrantTarget { | |
| 1646 | User, | |
| 1647 | Workspace, | |
| 1648 | } | |
| 1649 | ||
| 1650 | impl GrantTarget { | |
| 1651 | pub fn as_str(self) -> &'static str { | |
| 1652 | match self { | |
| 1653 | GrantTarget::User => "user", | |
| 1654 | GrantTarget::Workspace => "workspace", | |
| 1655 | } | |
| 1656 | } | |
| 1657 | } | |
| 1658 | ||
| 1659 | /// `admin_grant_invites`: gives a person (by username) or a workspace (by | |
| 1660 | /// slug) `amount` more invites; a negative amount takes some back. Returns | |
| 1661 | /// `Outcome<Allowance>`: theirs afterwards. | |
| 1662 | #[derive(Debug, Serialize, Deserialize)] | |
| 1663 | pub struct AdminGrantInvitesArgs { | |
| 1664 | pub target: GrantTarget, | |
| 1665 | pub name: String, | |
| 1666 | pub amount: i32, | |
| 1667 | #[serde(default)] | |
| 1668 | pub note: String, | |
| 1669 | pub staff: String, | |
| 1670 | } | |
| 1671 | ||
| 1672 | /// The most invites one grant gives or takes back. | |
| 1673 | pub const MAX_INVITE_GRANT: i32 = 1000; | |
| 1674 | ||
| 1675 | /// Invites staff granted. | |
| 1676 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1677 | #[serde(rename_all = "camelCase")] | |
| 1678 | pub struct InviteGrant { | |
| 1679 | pub amount: i32, | |
| 1680 | pub note: Option<String>, | |
| 1681 | pub granted_by: String, | |
| 1682 | /// RFC 3339. | |
| 1683 | pub created_at: String, | |
| 1684 | } | |
| 1685 | ||
| 1686 | /// Someone a person invited, and whom they invited in turn. | |
| 1687 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1688 | #[serde(rename_all = "camelCase")] | |
| 1689 | pub struct InviteTreeNode { | |
| 1690 | pub username: String, | |
| 1691 | /// When they used the invite. RFC 3339. | |
| 1692 | pub joined_at: String, | |
| 1693 | pub invited: Vec<InviteTreeNode>, | |
| 1694 | } | |
| 1695 | ||
| 1696 | /// `admin_invite_tree` (takes `UsernameArgs`): where a person came from | |
| 1697 | /// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`. | |
| 1698 | /// | |
| 1699 | /// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted | |
| 1700 | /// invites, grants and invites. Returns `Option<InviteTree>` with | |
| 1701 | /// `username` the slug and no `invited_by`. | |
| 1702 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1703 | #[serde(rename_all = "camelCase")] | |
| 1704 | pub struct InviteTree { | |
| 1705 | pub username: String, | |
| 1706 | /// Who invited them, then who invited that person, and so on. Empty | |
| 1707 | /// for an account made without an invite. | |
| 1708 | pub invited_by: Vec<String>, | |
| 1709 | /// The staff member who minted their invite, when staff did. | |
| 1710 | pub staff: Option<String>, | |
| 1711 | pub allowance: Allowance, | |
| 1712 | pub grants: Vec<InviteGrant>, | |
| 1713 | /// Their invites, newest first. | |
| 1714 | pub invites: Vec<Invite>, | |
| 1715 | /// Whom they invited, three levels down. | |
| 1716 | pub invited: Vec<InviteTreeNode>, | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 1717 | /// The shared invite link the account was made with, if it was. |
| 1718 | #[serde(default)] | |
| 1719 | pub shared: Option<SharedInviteSource>, | |
| 1720 | } | |
| 1721 | ||
| 1722 | // --- Shared invite links, staff only --- | |
| 1723 | // | |
| 1724 | // One link for a group (a conference's judges, a post, a community): up | |
| 1725 | // to `max_uses` new accounts, until it expires or staff revoke it, | |
| 1726 | // optionally only for addresses at some domains. Each use makes a new | |
| 1727 | // account, which then makes its own workspace; a shared link never joins | |
| 1728 | // anyone to an existing workspace, and uses nobody's allowance. Its code | |
| 1729 | // looks and is stored like any invite code (only a hash, and a sealed copy | |
| 1730 | // staff can copy again while it is live); the link is | |
| 1731 | // `https://g1t.sh/register?invite=<code>`. See | |
| 1732 | // services/identity/src/shared_invites.rs. | |
| 1733 | ||
| 1734 | /// How long a shared invite link works when staff give no date. | |
| 1735 | pub const SHARED_INVITE_TTL_DAYS: u64 = 14; | |
| 1736 | /// The furthest ahead a shared invite link's last day may be set. | |
| 1737 | pub const SHARED_INVITE_MAX_DAYS: u64 = 365; | |
| 1738 | /// The most accounts one shared invite link makes. | |
| 1739 | pub const MAX_SHARED_INVITE_USES: u32 = 1000; | |
| 1740 | /// The most characters a shared invite link's label keeps. | |
| 1741 | pub const MAX_SHARED_INVITE_LABEL: usize = 80; | |
| 1742 | /// The most email domains one shared invite link may be limited to. | |
| 1743 | pub const MAX_SHARED_INVITE_DOMAINS: usize = 10; | |
| 1744 | ||
| 1745 | /// Where a shared invite link stands. Only a live one makes accounts. | |
| 1746 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1747 | #[serde(rename_all = "snake_case")] | |
| 1748 | pub enum SharedInviteStatus { | |
| 1749 | Live, | |
| 1750 | /// Every use is taken. | |
| 1751 | UsedUp, | |
| 1752 | Expired, | |
| 1753 | Revoked, | |
| 1754 | } | |
| 1755 | ||
| 1756 | /// The shared invite link an account was made with. | |
| 1757 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1758 | pub struct SharedInviteSource { | |
| 1759 | pub id: String, | |
| 1760 | pub label: String, | |
| 1761 | } | |
| 1762 | ||
| 1763 | /// An account made with a shared invite link. | |
| 1764 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1765 | #[serde(rename_all = "camelCase")] | |
| 1766 | pub struct SharedInviteAccount { | |
| 1767 | /// Null once the account is purged. | |
| 1768 | pub username: Option<String>, | |
| 1769 | /// When it was made with the link. RFC 3339. | |
| 1770 | pub joined_at: String, | |
| 1771 | } | |
| 1772 | ||
| 1773 | /// One shared invite link, as staff see it. | |
| 1774 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1775 | #[serde(rename_all = "camelCase")] | |
| 1776 | pub struct SharedInvite { | |
| 1777 | /// `sinv_…`. | |
| 1778 | pub id: String, | |
| 1779 | /// Whom it is for, such as `Cloudflare judges`. | |
| 1780 | pub label: String, | |
| 1781 | /// The code, while it is live (and IDENTITY_KEY is set). | |
| 1782 | pub code: Option<String>, | |
| 1783 | /// The code's first group, such as `g1t-k7m2`. | |
| 1784 | pub hint: String, | |
| 1785 | pub max_uses: u32, | |
| 1786 | /// Accounts made with it so far. | |
| 1787 | pub uses: u32, | |
| 1788 | /// Only addresses at these domains may use it; empty for any. | |
| 1789 | pub domains: Vec<String>, | |
| 1790 | pub status: SharedInviteStatus, | |
| 1791 | /// The staff member who made it, by email. | |
| 1792 | pub staff: String, | |
| 1793 | /// RFC 3339. | |
| 1794 | pub created_at: String, | |
| 1795 | /// RFC 3339. | |
| 1796 | pub expires_at: String, | |
| 1797 | pub revoked_at: Option<String>, | |
| 1798 | pub revoked_by: Option<String>, | |
| 1799 | /// The accounts made with it, oldest first. | |
| 1800 | pub accounts: Vec<SharedInviteAccount>, | |
| 1801 | } | |
| 1802 | ||
| 1803 | /// `admin_shared_invites` takes `{}`: shared invite links, newest first, | |
| 1804 | /// at most 200, each with the accounts it made. Returns | |
| 1805 | /// `Vec<SharedInvite>`. | |
| 1806 | /// | |
| 1807 | /// `admin_create_shared_invite`: staff make a shared invite link. Recorded | |
| 1808 | /// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code. | |
| 1809 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 1810 | pub struct AdminCreateSharedInviteArgs { | |
| 1811 | /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters. | |
| 1812 | pub label: String, | |
| 1813 | /// 1 to [`MAX_SHARED_INVITE_USES`]. | |
| 1814 | pub max_uses: u32, | |
| 1815 | /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of | |
| 1816 | /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for | |
| 1817 | /// [`SHARED_INVITE_TTL_DAYS`] from now. | |
| 1818 | #[serde(default)] | |
| 1819 | pub expires_on: Option<String>, | |
| 1820 | /// Email domains it is limited to, such as `cloudflare.com`; empty for | |
| 1821 | /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`]. | |
| 1822 | #[serde(default)] | |
| 1823 | pub domains: Vec<String>, | |
| 1824 | /// The staff member, by email. | |
| 1825 | pub staff: String, | |
| 1826 | } | |
| 1827 | ||
| 1828 | /// `admin_revoke_shared_invite`: stops a shared invite link making any | |
| 1829 | /// more accounts. Those it made stay. Recorded in sudo's audit log. | |
| 1830 | /// Returns `Outcome<SharedInvite>`. | |
| 1831 | #[derive(Debug, Serialize, Deserialize)] | |
| 1832 | pub struct AdminRevokeSharedInviteArgs { | |
| 1833 | pub id: String, | |
| 1834 | pub staff: String, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1835 | } |
| 1836 | ||
| 1837 | #[cfg(test)] | |
| 1838 | mod deletion_tests { | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1839 | use super::{WorkspaceDeletion, protected_names}; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1840 | |
| 1841 | #[test] | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1842 | fn only_billing_or_protection_stands_in_the_way() { |
| 1843 | let clear = WorkspaceDeletion { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1844 | repositories: 2, |
| 1845 | projects: 1, | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1846 | members: 3, |
| 1847 | ..WorkspaceDeletion::default() | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1848 | }; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1849 | assert!(!clear.blocked()); |
| 1850 | assert_eq!(clear.reason("acme"), None); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1851 | let owing = WorkspaceDeletion { |
| 1852 | billing: Some("Pay first.".into()), | |
| 1853 | ..WorkspaceDeletion::default() | |
| 1854 | }; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1855 | assert!(owing.blocked()); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1856 | 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 member | 1857 | let protected = WorkspaceDeletion { |
| 1858 | billing: Some("Pay first.".into()), | |
| 1859 | protected: true, | |
| 1860 | ..WorkspaceDeletion::default() | |
| 1861 | }; | |
| 1862 | assert!(protected.blocked()); | |
| 1863 | assert_eq!( | |
| 1864 | protected.reason("flagon-io").as_deref(), | |
| 1865 | Some("flagon-io is protected and can never be deleted.") | |
| 1866 | ); | |
| 1867 | } | |
| 1868 | ||
| 1869 | #[test] | |
| 1870 | fn flagon_is_protected_whatever_the_variable_says() { | |
| 1871 | assert_eq!(protected_names(None), ["flagon-io"]); | |
| 1872 | assert_eq!(protected_names(Some("")), ["flagon-io"]); | |
| 1873 | assert_eq!(protected_names(Some(" , ")), ["flagon-io"]); | |
| 1874 | assert_eq!( | |
| 1875 | protected_names(Some("Flagon-IO, acme ,wsp_1")), | |
| 1876 | ["flagon-io", "acme", "wsp_1"] | |
| 1877 | ); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1878 | } |
| 1879 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.