Skip to content
1,155 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API reference examples use projects1//! The identity service: accounts, sessions, SSH keys and access tokens.
2//!
3//! Reached only through service bindings; see `g1t_contracts::identity` for
4//! the methods and their arguments.
5
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6mod access;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)7mod account_deletion;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace8mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'9mod aliases;
Workspace names and icons, and a component kit for every control10mod avatars;
API reference examples use projects11mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12mod deletion;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca13mod deploy_keys;
API reference examples use projects14mod device;
Search across all of g1t, Explore, and a command palette15mod directory;
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.16mod dock;
API reference examples use projects17mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look18mod emails;
19mod github;
20mod invites;
Merge main (membership, two-factor, GitHub repo roles) into tokens21mod members;
API reference examples use projects22mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person23mod paid;
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.24mod people;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains25mod profiles;
26mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'27mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API28mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29mod security;
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)30mod shared_invites;
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.31mod sounds;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar32mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity34mod token_reach;
API reference examples use projects35mod tokens;
Merge main (membership, two-factor, GitHub repo roles) into tokens36mod two_factor;
API reference examples use projects37mod workspaces;
38
39use g1t_contracts::identity::*;
40use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers41use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_username, new_id};
API reference examples use projects42use g1t_kit::{args, now_ms, reply, rpc_method};
43use serde::Deserialize;
44use tokens::TOKEN_PREFIX;
45use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas46use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API reference examples use projects47
48const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
49const RESET_TTL_SECONDS: u64 = 60 * 60;
50const MIN_PASSWORD_LENGTH: usize = 10;
51const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
52
53/// A user as selected from the database; `verified` arrives as 0 or 1.
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers54/// What a username may be, as registration and GitHub sign-up say when one is refused.
55pub(crate) const USERNAME_RULES: &str =
56 "Usernames use letters of either case, digits and single hyphens, up to 39 characters, not starting or ending with a hyphen, and cannot be a reserved word.";
57
API reference examples use projects58#[derive(Deserialize)]
59struct Account {
60 id: String,
61 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 managers62 /// Selected where the person is shown: their username as they wrote it.
63 #[serde(default)]
64 display_username: Option<String>,
API reference examples use projects65 verified: u8,
Workspace names and icons, and a component kit for every control66 /// Selected only where the person is being shown to themselves.
67 #[serde(default)]
68 avatar: Option<String>,
API reference examples use projects69}
70
71impl From<Account> for User {
72 fn from(row: Account) -> Self {
73 User {
74 id: row.id,
75 username: row.username,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers76 display_username: row.display_username,
API reference examples use projects77 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control78 avatar: row.avatar,
API reference examples use projects79 ..User::default()
80 }
81 }
82}
83
84#[derive(Deserialize)]
85struct UserRow {
86 id: String,
87 username: String,
88 password_hash: String,
89 verified: u8,
90}
91
92/// The owner of an emailed token.
93#[derive(Deserialize)]
94struct TokenOwner {
95 id: String,
96 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 /// The address a link was sent to; null on links from before accounts
98 /// had several, which are for the primary.
99 #[serde(default)]
100 email_id: Option<String>,
API reference examples use projects101}
102
103#[derive(Deserialize)]
104struct KeyRow {
105 id: String,
106 title: String,
107 fingerprint: String,
108 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca109 #[serde(default)]
110 last_used_at: Option<String>,
API reference examples use projects111}
112
113impl From<KeyRow> for SshKey {
114 fn from(row: KeyRow) -> Self {
115 SshKey {
116 id: row.id,
117 title: row.title,
118 fingerprint: row.fingerprint,
119 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca120 last_used_at: row.last_used_at,
API reference examples use projects121 }
122 }
123}
124
125struct Identity {
126 db: D1Database,
127 env: Env,
128}
129
130impl Identity {
131 /// Runs a query that returns at most one user, for showing to others:
132 /// without their workspaces.
133 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
134 Ok(self
135 .db
136 .prepare(sql)
137 .bind(&[JsValue::from(param)])?
138 .first::<Account>(None)
139 .await?
140 .map(User::from))
141 }
142
143 /// Attaches the workspaces a user belongs to, so that any service can
144 /// authorize them without asking again.
145 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
146 let Some(mut user) = user else {
147 return Ok(None);
148 };
Merge main (membership, two-factor, GitHub repo roles) into tokens149 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look150 // Roles on single repositories, under the same policy (access.rs).
151 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens152 // Access to a workspace is used only within its policy; see security.rs.
153 let within = self.within_policy(&user.id, memberships, grants).await?;
154 user.workspaces = within.memberships;
155 user.grants = within.grants;
156 user.held = within.held;
API reference examples use projects157 Ok(Some(user))
158 }
159
160 /// Runs a query that resolves credentials to at most one user.
161 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
162 let user = self.find_public_user(sql, param).await?;
163 self.with_workspaces(user).await
164 }
165
166 /// Consumes a token of `kind`, returning its owner if it was valid.
167 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
168 let id = crypto::sha256_hex(token);
169 let owner = self
170 .db
171 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look172 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
API reference examples use projects173 JOIN users ON users.id = email_tokens.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)174 WHERE email_tokens.id = ? AND email_tokens.kind = ? AND users.deleted_at IS NULL
API reference examples use projects175 AND email_tokens.expires_at > {SQL_NOW}"
176 ))
177 .bind(&[id.as_str().into(), kind.into()])?
178 .first::<TokenOwner>(None)
179 .await?;
180 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look181 // Every outstanding token of this kind dies with the one used:
182 // every reset link, and every confirmation link for the same
183 // address (another address's links still work).
API reference examples use projects184 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look185 .prepare(
186 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
187 AND (?2 = 'reset' OR email_id IS ?3)",
188 )
189 .bind(&[
190 owner.id.as_str().into(),
191 kind.into(),
192 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
193 ])?
API reference examples use projects194 .run()
195 .await?;
196 }
197 Ok(owner)
198 }
199
200 async fn resend_verification(&self, a: UserArgs) -> Result<Outcome<bool>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look201 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
202 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
203 }
204 self.resend_primary(&a.user).await
API reference examples use projects205 }
206
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)207 /// The link in a confirmation email, followed: signed in or not. It
208 /// ends the code sent with it (emails.rs).
209 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<g1t_contracts::accounts::EmailConfirmed>> {
API reference examples use projects210 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
211 return Ok(Outcome::fail(
212 FailureCode::Invalid,
213 "This confirmation link is not valid or has expired.",
214 ));
215 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)216 self.confirm_address(&owner.id, owner.email_id.as_deref()).await
API reference examples use projects217 }
218
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look219 /// Any confirmed address of an account can ask for a reset; so can the
220 /// unconfirmed address a new account signed up with. See emails.rs.
API reference examples use projects221 async fn request_password_reset(&self, a: EmailArgs) -> Result<bool> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look222 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
223 && match a.client.as_deref() {
224 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
225 None => true,
226 };
227 if allowed && let Some(target) = self.reset_target(&a.email).await? {
Identity: a password reset for a known address that fails to save or send is logged, never answered, so the reply never says an account exists228 // A failure from here on happens only for a real account, so it
229 // is logged, never answered: the reply below stays the same.
230 if let Err(error) = self.send_reset(&target).await {
231 worker::console_error!("password reset for a known address failed: {error}");
232 }
233 }
234 // The same answer either way, so addresses cannot be probed.
235 Ok(true)
236 }
237
238 /// Saves a reset link for `target` and mails it, telling the account's
239 /// other addresses.
240 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
241 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 let token = crypto::random_hex(32);
243 self.db
244 .prepare(format!(
245 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
246 VALUES (?, ?, 'reset', {}, ?)",
247 sql_after(RESET_TTL_SECONDS)
248 ))
249 .bind(&[
250 crypto::sha256_hex(&token).into(),
251 target.user_id.as_str().into(),
252 target.email_id.as_str().into(),
253 ])?
254 .run()
API reference examples use projects255 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look256 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
257 // The primary and the backup hear of it when it went elsewhere.
258 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
259 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
260 let change = format!("A password reset was asked for through {}", target.display);
261 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
262 worker::console_error!("security notice failed: {error}");
263 }
264 }
API reference examples use projects265 }
Identity: a password reset for a known address that fails to save or send is logged, never answered, so the reply never says an account exists266 Ok(())
API reference examples use projects267 }
268
269 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
270 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
271 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
272 }
273 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
274 return Ok(Outcome::fail(
275 FailureCode::Invalid,
276 "This reset link is not valid or has expired.",
277 ));
278 };
279 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look280 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
API reference examples use projects281 .bind(&[
282 crypto::hash_password(&a.password).into(),
283 owner.id.as_str().into(),
284 ])?
285 .run()
286 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look287 // Following an emailed link also proves the address it went to
288 // (unless another account confirmed it first).
289 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
290 // Anyone signed in with the old password is signed out, and nobody
291 // stays locked out by the wrong guesses before it.
API reference examples use projects292 self.db
293 .prepare("DELETE FROM sessions WHERE user_id = ?")
294 .bind(&[owner.id.as_str().into()])?
295 .run()
296 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look297 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
298 self.log_security(&owner.id, "password_changed", None, None).await;
299 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
300 let verified = self
301 .find_public_user(
302 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
303 &owner.id,
304 )
305 .await?
306 .is_some_and(|user| user.verified);
API reference examples use projects307 Ok(Outcome::Ok(User {
308 id: owner.id,
309 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310 verified,
API reference examples use projects311 ..User::default()
312 }))
313 }
314
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look315 /// The account a login names: a username, or any confirmed address.
316 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
317 let login = login.trim().to_lowercase();
318 let (column, value) = if login.contains('@') {
319 match self.user_with_verified_email(&login).await? {
320 Some(id) => ("id", id),
321 None => return Ok(None),
322 }
323 } else {
324 ("username", login)
325 };
326 self.db
327 .prepare(format!(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)328 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users
329 WHERE {column} = ? AND deleted_at IS NULL"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look330 ))
331 .bind(&[JsValue::from(value)])?
API reference examples use projects332 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 .await
334 }
335
336 /// Checks a password for a login, throttled (see throttle.rs). The
337 /// refusal is one of two messages, the same for every account.
338 async fn checked_password(
339 &self,
340 login: &str,
341 password: &str,
342 client: Option<&str>,
343 ) -> Result<std::result::Result<User, &'static str>> {
344 let row = self.password_row(login).await?;
345 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
346 let (account_key, client_key) = Identity::password_keys(&subject, client);
347 if self.password_locked(&account_key, client_key.as_deref()).await? {
348 return Ok(Err(throttle::THROTTLED));
349 }
350 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
351 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
352 Some(row) => {
353 self.clear(&account_key).await?;
354 Ok(Ok(User {
355 id: row.id,
356 username: row.username,
357 verified: row.verified != 0,
358 ..User::default()
359 }))
360 }
361 None => {
362 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
363 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
364 Ok(Err("Incorrect username or password."))
365 }
366 }
367 }
368
Merge main (membership, two-factor, GitHub repo roles) into tokens369 /// Git over HTTPS with the account's password. With two-factor
370 /// authentication on, a password alone is never enough: use an access
371 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look372 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
373 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens374 if let Some(user) = &user
375 && self.two_factor_enabled(&user.id).await?
376 {
377 return Ok(None);
378 }
API reference examples use projects379 self.with_workspaces(user).await
380 }
381
382 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers383 // Kept as typed for showing; found, linked and mentioned lowercased.
384 let chosen = claimable_username(&a.username);
385 let username = chosen.as_ref().map_or_else(|| a.username.trim().to_lowercase(), |name| name.canonical.clone());
386 let claimable = chosen.is_some();
API reference examples use projects387 let email = a.email.trim().to_lowercase();
388 let invalid = |message: &str| Ok(Outcome::fail(FailureCode::Invalid, message));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look389 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
390 // The invite first: without one, nothing else on the form matters.
391 if self.invites_required() && invite_code.is_none() {
392 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
393 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent394 if !claimable {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers395 return invalid(USERNAME_RULES);
API reference examples use projects396 }
397 let well_formed_email = email
398 .split_once('@')
399 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
400 && !email.contains(char::is_whitespace);
401 if !well_formed_email {
402 return invalid("Enter a valid email address.");
403 }
404 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
405 return invalid(PASSWORD_TOO_SHORT);
406 }
407 let taken = self
408 .db
409 // Usernames and workspaces share one namespace, so that a name
410 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look411 // An address is taken once an account has confirmed it; an
412 // unconfirmed one goes to whoever confirms it first (emails.rs).
API reference examples use projects413 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look414 "SELECT username FROM users WHERE username = ?
415 UNION ALL SELECT email FROM user_emails WHERE email = ? AND verified_at IS NOT NULL
API reference examples use projects416 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
417 )
418 .bind(&[
419 username.as_str().into(),
420 email.as_str().into(),
421 username.as_str().into(),
422 ])?
423 .first::<serde_json::Value>(None)
424 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look425 // A renamed workspace's old slug stays reserved for it a while, and
426 // a deleted workspace's for good.
427 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API reference examples use projects428 return Ok(Outcome::fail(
429 FailureCode::Conflict,
430 "That username or email is already registered.",
431 ));
432 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look433 let password_hash = crypto::hash_password(&a.password);
434 let user = match self
435 .create_account(invites::NewAccount {
436 username: &username,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers437 display_username: chosen.as_ref().and_then(|name| name.display_if_cased()),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look438 email: &email,
439 password_hash: &password_hash,
440 verified: false,
441 invite_code,
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 confirm442 email_proof: a.email_proof.as_deref(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look443 client: a.client.as_deref(),
444 })
445 .await?
446 {
447 Outcome::Ok(user) => user,
448 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API reference examples use projects449 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)450 // The account exists either way; the email can be sent again from
451 // the confirmation page. It carries a code and a link (emails.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas452 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)453 && let Err(error) = self.send_primary_confirmation(&user.id, &user.username).await
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas454 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)455 worker::console_error!("confirmation email failed: {error}");
API reference examples use projects456 }
457 self.start_session(user).await
458 }
459
460 async fn sign_in(&self, a: SignInArgs) -> Result<Outcome<SignedIn>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look461 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
462 Ok(user) => user,
463 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API reference examples use projects464 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API reference examples use projects466 self.start_session(user).await
467 }
468
Merge main (membership, two-factor, GitHub repo roles) into tokens469 /// Starts a session for someone who just proved their password (or
470 /// GitHub account). With two-factor authentication on, it starts none:
471 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API reference examples use projects472 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens473 if self.two_factor_enabled(&user.id).await? {
474 let challenge = self.issue_challenge(&user.id).await?;
475 return Ok(Outcome::Ok(SignedIn {
476 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
477 session_token: String::new(),
478 two_factor_challenge: Some(challenge),
479 }));
480 }
481 self.session_for(user).await
482 }
483
484 /// A new session for `user`, who has proved who they are in full.
485 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)486 // Whichever way it was proved, a deleted account starts none
487 // (account_deletion.rs).
488 if !self.account_live(&user.id).await? {
489 return Ok(Outcome::fail(FailureCode::Unauthenticated, "Incorrect username or password."));
490 }
API reference examples use projects491 let session_token = crypto::random_hex(32);
492 self.db
493 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look494 // Signing in is proof it is the person: see security.rs.
495 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
API reference examples use projects496 sql_after(SESSION_TTL_SECONDS)
497 ))
498 .bind(&[
499 crypto::sha256_hex(&session_token).into(),
500 user.id.as_str().into(),
501 ])?
502 .run()
503 .await?;
504 Ok(Outcome::Ok(SignedIn {
505 user,
506 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens507 two_factor_challenge: None,
API reference examples use projects508 }))
509 }
510
511 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
512 self.db
513 .prepare("DELETE FROM sessions WHERE id = ?")
514 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
515 .run()
516 .await?;
517 Ok(())
518 }
519
520 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
521 self.find_user(
522 &format!(
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers523 "SELECT users.id, users.username, users.display_username, users.email_verified_at IS NOT NULL AS verified,
Workspace names and icons, and a component kit for every control524 users.avatar
API reference examples use projects525 FROM sessions JOIN users ON users.id = sessions.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)526 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW} AND users.deleted_at IS NULL"
API reference examples use projects527 ),
528 &crypto::sha256_hex(&a.session_token),
529 )
530 .await
531 }
532
533 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
534 // Like GitHub, a token alone identifies its user.
535 if a.secret.starts_with(TOKEN_PREFIX) {
536 self.user_for_access_token(&a.secret).await
537 } else {
538 self.user_for_password(&a.username, &a.secret).await
539 }
540 }
541
542 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
543 self.find_user(
544 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified FROM ssh_keys
545 JOIN users ON users.id = ssh_keys.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)546 WHERE fingerprint = ? AND users.deleted_at IS NULL",
API reference examples use projects547 &a.fingerprint,
548 )
549 .await
550 }
551
552 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
553 self.find_public_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)554 // A deleted account is nobody's to find, mention or add.
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers555 "SELECT id, username, display_username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
API reference examples use projects556 &a.username.to_lowercase(),
557 )
558 .await
559 }
560
Inbox: threads, reasons, subscriptions and watching561 /// `notify_by_email`: an inbox item, emailed to the person it is for,
562 /// only at a confirmed address and only while they can still read the
563 /// repository it is about. Returns whether it was sent.
564 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
565 #[derive(Deserialize)]
566 struct Address {
567 email: Option<String>,
568 }
569 let user = self
570 .find_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)571 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
Inbox: threads, reasons, subscriptions and watching572 &a.username.to_lowercase(),
573 )
574 .await?;
575 let Some(user) = user.filter(|user| user.verified) else {
576 return Ok(false);
577 };
578 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
579 &self.env.service("REPOS")?,
580 "readable",
581 &g1t_contracts::repos::ReadableArgs {
582 ids: vec![a.repo_id.clone()],
583 viewer: Some(user.clone()),
584 },
585 )
586 .await?;
587 if readable.is_empty() {
588 return Ok(false);
589 }
590 let address = self
591 .db
592 .prepare("SELECT email FROM users WHERE id = ?")
593 .bind(&[user.id.as_str().into()])?
594 .first::<Address>(None)
595 .await?
596 .and_then(|row| row.email)
597 .filter(|email| !email.trim().is_empty());
598 let Some(address) = address else {
599 return Ok(false);
600 };
601 email::send_notification(&self.env, &address, &a).await?;
602 Ok(true)
603 }
604
API reference examples use projects605 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
606 #[derive(serde::Deserialize)]
607 struct Named {
608 id: String,
609 name: String,
610 }
611 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
612 let mut names = std::collections::HashMap::new();
613 if ids.is_empty() {
614 return Ok(names);
615 }
616 let marks = vec!["?"; ids.len()].join(", ");
617 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
618 for sql in [
619 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
620 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
621 ] {
622 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
623 names.insert(row.id, row.name);
624 }
625 }
626 Ok(names)
627 }
628
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)629 /// `users_for_audience`: the people behind these ids (at most 50), each
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.630 /// with their workspaces, roles, base permissions and repository
631 /// grants, under each workspace's policy, as a signed-in viewer would
632 /// have them. For the agents service, which answers only with what
633 /// every person who will read the answer may see
634 /// (docs.g1t.sh/guides/agent-access/, "What an agent can and can't
635 /// know"). Ids of no live account are left out, so the caller can tell
636 /// someone it could not resolve. Reached only by service binding.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)637 async fn users_for_audience(&self, a: UsernamesArgs) -> Result<Vec<User>> {
638 let mut found = Vec::new();
639 for id in a.ids.iter().take(50) {
640 let user = self
641 .find_user(
642 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ? AND deleted_at IS NULL",
643 id,
644 )
645 .await?;
646 if let Some(user) = user {
647 found.push(user);
648 }
649 }
650 Ok(found)
651 }
652
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97653 /// `accounts`: the accounts behind these ids (at most 200), each with
654 /// its username and avatar, for lists that keep ids, such as who
655 /// starred a repository. Ids of no account are left out.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar656 /// `display_usernames`: each lowercased username's chosen case, for the
657 /// API, which shows it beside every username it answers with.
658 async fn display_usernames(&self, a: DisplayUsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
659 #[derive(serde::Deserialize)]
660 struct Row {
661 username: String,
662 display_username: String,
663 }
664 let mut names: Vec<String> = a.usernames.iter().map(|name| name.trim().to_lowercase()).filter(|name| !name.is_empty()).collect();
665 names.sort();
666 names.dedup();
667 names.truncate(200);
668 let mut found = std::collections::HashMap::new();
669 if names.is_empty() {
670 return Ok(found);
671 }
672 let marks = vec!["?"; names.len()].join(", ");
673 let bind: Vec<worker::wasm_bindgen::JsValue> = names.iter().map(|name| name.as_str().into()).collect();
674 let rows = self
675 .db
676 .prepare(format!(
677 "SELECT username, display_username FROM users WHERE username IN ({marks}) AND display_username IS NOT NULL AND deleted_at IS NULL"
678 ))
679 .bind(&bind)?
680 .all()
681 .await?
682 .results::<Row>()?;
683 for row in rows {
684 if row.display_username.eq_ignore_ascii_case(&row.username) && row.display_username != row.username {
685 found.insert(row.username, row.display_username);
686 }
687 }
688 Ok(found)
689 }
690
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97691 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
692 #[derive(serde::Deserialize)]
693 struct Row {
694 id: String,
695 username: String,
696 avatar: Option<String>,
697 }
698 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
699 let mut found = std::collections::HashMap::new();
700 if ids.is_empty() {
701 return Ok(found);
702 }
703 let marks = vec!["?"; ids.len()].join(", ");
704 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
705 let rows = self
706 .db
707 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
708 .bind(&bind)?
709 .all()
710 .await?
711 .results::<Row>()?;
712 for row in rows {
713 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
714 }
715 Ok(found)
716 }
717
API reference examples use projects718 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
719 let rows = self
720 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca721 .prepare("SELECT id, title, fingerprint, created_at, last_used_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
API reference examples use projects722 .bind(&[a.user.id.into()])?
723 .all()
724 .await?
725 .results::<KeyRow>()?;
726 Ok(rows.into_iter().map(SshKey::from).collect())
727 }
728
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge729 /// The account (user id) that registered each key, by fingerprint
730 /// (`SHA256:…`). At most 100; unknown keys are left out.
731 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
732 #[derive(serde::Deserialize)]
733 struct Row {
734 fingerprint: String,
735 user_id: String,
736 }
737 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
738 if fingerprints.is_empty() {
739 return Ok(std::collections::HashMap::new());
740 }
741 let marks = vec!["?"; fingerprints.len()].join(", ");
742 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
743 Ok(self
744 .db
745 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
746 .bind(&binds)?
747 .all()
748 .await?
749 .results::<Row>()?
750 .into_iter()
751 .map(|row| (row.fingerprint, row.user_id))
752 .collect())
753 }
754
API reference examples use projects755 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
756 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
757 return Ok(Outcome::fail(
758 FailureCode::Invalid,
759 "That is not a valid OpenSSH public key.",
760 ));
761 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca762 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
763 if self.key_in_use(&key.fingerprint).await? {
764 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API reference examples use projects765 }
766 let now = now_ms();
767 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
768 .into_iter()
769 .find(|candidate| !candidate.is_empty())
770 .unwrap_or_default()
771 .to_owned();
772 let row = KeyRow {
773 id: new_id("key", now),
774 title,
775 fingerprint: key.fingerprint,
776 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca777 last_used_at: None,
API reference examples use projects778 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca779 let inserted = self.db
API reference examples use projects780 .prepare(
781 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
782 VALUES (?, ?, ?, ?, ?, ?)",
783 )
784 .bind(&[
785 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca786 a.user.id.as_str().into(),
API reference examples use projects787 row.title.as_str().into(),
788 key.public_key.into(),
789 row.fingerprint.as_str().into(),
790 row.created_at.as_str().into(),
791 ])?
792 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca793 .await;
794 // Added at the same moment elsewhere: the trigger or the unique
795 // index refused it.
796 if let Err(error) = inserted {
797 if self.key_in_use(&row.fingerprint).await? {
798 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
799 }
800 return Err(error);
801 }
Merge main (membership, two-factor, GitHub repo roles) into tokens802 let shown = format!("{} ({})", row.title, row.fingerprint);
803 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
804 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API reference examples use projects805 Ok(Outcome::Ok(row.into()))
806 }
807
Merge main (membership, two-factor, GitHub repo roles) into tokens808 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca809 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens810 #[derive(Deserialize)]
811 struct Removed {
812 title: String,
813 fingerprint: String,
814 }
815 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca816 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens817 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca818 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens819 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca820 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens821 if let Some(removed) = removed {
822 let shown = format!("{} ({})", removed.title, removed.fingerprint);
823 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
824 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
825 }
API reference examples use projects826 Ok(())
827 }
828}
829
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas830/// Every 15 minutes: staff hear about waitlist requests that arrived while
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member831/// the last summary's window was still open, so none waits on a later one;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)832/// and deleted workspaces and accounts past their restore window are purged
833/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas834#[event(scheduled)]
835async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
836 let Ok(db) = env.d1("DB") else { return };
837 let identity = Identity { db, env };
838 if let Err(error) = identity.notify_staff_of_requests().await {
839 worker::console_error!("waitlist summary: {error}");
840 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member841 if let Err(error) = identity.purge_due_workspaces().await {
842 worker::console_error!("workspace purge: {error}");
843 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)844 // And deleted accounts past theirs (account_deletion.rs).
845 if let Err(error) = identity.purge_due_accounts().await {
846 worker::console_error!("account purge: {error}");
847 }
Merge main (membership, two-factor, GitHub repo roles) into tokens848 // Once: creators of repositories made before they got Admin (members.rs).
849 if let Err(error) = identity.backfill_creator_grants().await {
850 worker::console_error!("creator grants: {error}");
851 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas852}
853
API reference examples use projects854#[event(fetch)]
855async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
856 let Some(method) = rpc_method(&request) else {
857 return Response::error("Not found", 404);
858 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents859 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
860 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API reference examples use projects861 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents862 let identity = Identity { db, env };
API reference examples use projects863
Fast pages, required checks on the branch, self-hosted runners, honest incidents864 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette865 "register" => {
866 let outcome = identity.register(args(body)?).await?;
867 if let Outcome::Ok(signed_in) = &outcome {
868 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
869 }
870 reply(&outcome)
871 }
API reference examples use projects872 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette873 "create_workspace" => {
874 let outcome = identity.create_workspace(args(body)?).await?;
875 if let Outcome::Ok(workspace) = &outcome {
876 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
877 }
878 reply(&outcome)
879 }
API reference examples use projects880 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
881 "list_members" => reply(&identity.list_members(args(body)?).await?),
882 "add_member" => reply(&identity.add_member(args(body)?).await?),
883 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens884 // Owners, roles, leaving and member privileges; see members.rs.
885 "update_member" => reply(&identity.update_member(args(body)?).await?),
886 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
887 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
888 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
889 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
890 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette891 "update_workspace" => {
892 let outcome = identity.update_workspace(args(body)?).await?;
893 if let Outcome::Ok(workspace) = &outcome {
894 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
895 }
896 reply(&outcome)
897 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains898 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
899 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
900 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'901 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look902 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
903 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
904 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette905 "set_workspace_avatar" => {
906 let outcome = identity.set_workspace_avatar(args(body)?).await?;
907 if let Outcome::Ok(workspace) = &outcome {
908 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
909 }
910 reply(&outcome)
911 }
912 "set_user_avatar" => {
913 let a: SetUserAvatarArgs = args(body)?;
914 let (username, id) = (a.user.username.clone(), a.user.id.clone());
915 let outcome = identity.set_user_avatar(a).await?;
916 if matches!(outcome, Outcome::Ok(_)) {
917 identity.announce_user(&username, Some(&id)).await;
918 }
919 reply(&outcome)
920 }
API reference examples use projects921 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
922 "remove_workspace_token" => reply(&identity.remove_workspace_token(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look923 // Signing in with GitHub; see github.rs.
924 "github_enabled" => reply(&identity.github_enabled()),
925 "github_start" => reply(&identity.github_start(args(body)?).await?),
926 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
927 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
928 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
929 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
930 "github_account" => reply(&identity.github_account(args(body)?).await?),
931 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
932 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
933 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
934 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
API reference examples use projects935 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
936 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
937 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
938 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
939 "revoke_oauth_grant" => reply(&identity.revoke_oauth_grant(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step940 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
API reference examples use projects941 "device_start" => reply(&identity.device_start(args(body)?).await?),
942 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
943 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
944 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
945 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
946 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)947 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
948 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
API reference examples use projects949 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
950 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look951 // A person's email addresses; see emails.rs and security.rs.
952 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
953 "add_email" => reply(&identity.add_email(args(body)?).await?),
954 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
955 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
956 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
957 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens958 // Two-factor authentication; see two_factor.rs.
959 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
960 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
961 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
962 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
963 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
964 "two_factor_sign_in" => reply(&identity.two_factor_sign_in(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look965 "security_log" => reply(&identity.security_log(args(body)?).await?),
966 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
967 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
968 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
969 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
970 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API reference examples use projects971 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
972 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
973 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
974 "user_for_access_token" => {
975 let a: TokenArgs = args(body)?;
976 reply(&identity.user_for_access_token(&a.token).await?)
977 }
978 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
979 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
980 "usernames" => reply(&identity.usernames(args(body)?).await?),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar981 // The API: each username's chosen case, shown beside it.
982 "display_usernames" => reply(&identity.display_usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97983 "accounts" => reply(&identity.accounts(args(body)?).await?),
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)984 "users_for_audience" => reply(&identity.users_for_audience(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching985 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains986 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette987 "update_profile" => {
988 let outcome = identity.update_profile(args(body)?).await?;
989 if let Outcome::Ok(profile) = &outcome {
990 identity.announce_user(&profile.username, None).await;
991 }
992 reply(&outcome)
993 }
994 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains995 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
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.996 // Each person's dock pins, per workspace; see dock.rs.
997 "dock_pins" => reply(&identity.dock_pins(args(body)?).await?),
998 "set_dock_pins" => reply(&identity.set_dock_pins(args(body)?).await?),
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.999 // Each person's chat sounds and desktop notifications; see sounds.rs.
1000 "sound_settings" => reply(&identity.sound_settings(args(body)?).await?),
1001 "set_sound_settings" => reply(&identity.set_sound_settings(args(body)?).await?),
API reference examples use projects1002 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1003 // Services only: who registered each key, for verifying commit
1004 // signatures (repos' signatures.rs).
1005 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API reference examples use projects1006 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1007 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
1008 // A repository's deploy keys, and who an SSH key signs in as; see
1009 // deploy_keys.rs.
1010 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
1011 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
1012 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
1013 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
1014 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API reference examples use projects1015 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
1016 "create_access_token" => reply(&identity.create_access_token(args(body)?).await?),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1017 // Making and changing a person's or a workspace's token, and
1018 // workspaces' rules for tokens; see token_reach.rs.
1019 "create_token" => reply(&identity.create_token(args(body)?).await?),
1020 "update_token" => reply(&identity.update_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity1021 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
1022 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
1023 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
1024 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
1025 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
API reference examples use projects1026 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
1027 "agent_scope" => reply(&identity.agent_scope(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1028 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
1029 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
1030 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'1031 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
1032 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens1033 "remove_access_token" => reply(&identity.remove_access_token(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1034 // Invites and the waitlist; see invites.rs.
1035 "registration" => reply(&identity.registration_mode()),
1036 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
1037 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
1038 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
1039 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
1040 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
1041 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
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)1042 "list_invitations" => reply(&identity.list_invitations(args(body)?).await?),
1043 "accept_invitation" => reply(&identity.accept_invitation(args(body)?).await?),
1044 "decline_invitation" => reply(&identity.decline_invitation(args(body)?).await?),
1045 "find_people" => reply(&identity.find_people(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1046 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
1047 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.1048 "resend_workspace_invite" => reply(&identity.resend_workspace_invite(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1049 "request_access" => reply(&identity.request_access(args(body)?).await?),
1050 // Who has access to a repository; see access.rs.
1051 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
1052 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
1053 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
1054 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
1055 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
1056 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
1057 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
1058 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
1059 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'1060 // Where a workspace keeps its repositories' git data (EU residency).
1061 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
1062 "set_workspace_residency" => reply(&identity.set_workspace_residency(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1063 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1064 "forget_repo_access" => {
1065 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
1066 // A purged repository's deploy keys go with its access.
1067 identity.forget_deploy_keys(&a.repo_id).await?;
1068 reply(&identity.forget_repo_access(a).await?)
1069 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1070 // Teams (teams.rs).
1071 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
1072 "get_team" => reply(&identity.get_team(args(body)?).await?),
1073 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1074 "set_team_creation" => reply(&identity.set_team_creation(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1075 "update_team" => reply(&identity.update_team(args(body)?).await?),
1076 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
1077 "team_members" => reply(&identity.team_members(args(body)?).await?),
1078 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
1079 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
1080 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
1081 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
1082 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
1083 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
1084 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1085 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1086 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.1087 // Agents on teams (teams.rs), and the people directory (people.rs).
1088 "team_agents" => reply(&identity.team_agents(args(body)?).await?),
1089 "set_team_agent" => reply(&identity.set_team_agent(args(body)?).await?),
1090 "remove_team_agent" => reply(&identity.remove_team_agent(args(body)?).await?),
1091 "agent_teams" => reply(&identity.agent_teams(args(body)?).await?),
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.1092 "team_agent_index" => reply(&identity.team_agent_index(args(body)?).await?),
1093 "adopt_agent_teams" => reply(&identity.adopt_agent_teams(args(body)?).await?),
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.1094 "people_directory" => reply(&identity.people_directory(args(body)?).await?),
1095 "set_member_profile" => reply(&identity.set_member_profile(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1096 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1097 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1098 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1099 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1100 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1101 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1102 "admin_decide_waitlist" => reply(&identity.admin_decide_waitlist(args(body)?).await?),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1103 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1104 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1105 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1106 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1107 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1108 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1109 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1110 // Shared invite links for a group; see shared_invites.rs.
1111 "admin_shared_invites" => reply(&identity.admin_shared_invites().await?),
1112 "admin_create_shared_invite" => reply(&identity.admin_create_shared_invite(args(body)?).await?),
1113 "admin_revoke_shared_invite" => reply(&identity.admin_revoke_shared_invite(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1114 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1115 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1116 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1117 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)1118 // Deleting accounts (account_deletion.rs): the person from the
1119 // site, staff from sudo. There is no API route for it.
1120 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1121 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1122 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1123 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1124 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1125 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1126 // Workspace aliases, set by staff only; see aliases.rs.
1127 "admin_aliases" => reply(&identity.admin_aliases().await?),
1128 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1129 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API reference examples use projects1130 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1131 };
1132 served.finish(answered)
API reference examples use projects1133}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1134
1135#[cfg(test)]
1136mod register_tests {
1137 use super::*;
1138
1139 #[test]
1140 fn nobody_registers_as_g1t() {
1141 // What register checks the username with, whatever its case.
1142 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1143 assert!(claimable_username(username).is_none(), "{username}");
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1144 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1145 assert_eq!(claimable_username("ana").map(|name| name.canonical).as_deref(), Some("ana"));
1146 }
1147
1148 #[test]
1149 fn a_username_keeps_the_case_it_was_chosen_in() {
1150 let name = claimable_username("Ana-Lopez").unwrap();
1151 assert_eq!(name.canonical, "ana-lopez");
1152 assert_eq!(name.display_if_cased(), Some("Ana-Lopez"));
1153 assert!(USERNAME_RULES.contains("either case"));
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1154 }
1155}

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