Skip to content
1,028 linesCodeBlameRaw

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

API and MCP server, Rust identity service, registration, site redesign1//! The identity service: accounts, 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;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'8mod aliases;
Workspace names and icons, and a component kit for every control9mod avatars;
API and MCP server, Rust identity service, registration, site redesign10mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11mod deletion;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca12mod deploy_keys;
Device sign-in replaces registering and minting tokens over the API13mod device;
Search across all of g1t, Explore, and a command palette14mod directory;
Email verification, password reset, and Git for AI scale positioning15mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look16mod emails;
17mod github;
18mod invites;
Merge main (membership, two-factor, GitHub repo roles) into tokens19mod members;
OAuth 2.1 sign-in for MCP clients and other applications20mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person21mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains22mod profiles;
23mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'24mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API25mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar27mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity29mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell30mod tokens;
Merge main (membership, two-factor, GitHub repo roles) into tokens31mod two_factor;
Workspaces own repositories32mod workspaces;
API and MCP server, Rust identity service, registration, site redesign33
34use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos35use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent36use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign37use g1t_kit::{args, now_ms, reply, rpc_method};
38use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell39use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign40use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas41use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign42
RFC 3339 timestamps in identity and repos43const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
44const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign45const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning46const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
47
48/// A user as selected from the database; `verified` arrives as 0 or 1.
49#[derive(Deserialize)]
50struct Account {
51 id: String,
52 username: String,
53 verified: u8,
Workspace names and icons, and a component kit for every control54 /// Selected only where the person is being shown to themselves.
55 #[serde(default)]
56 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning57}
58
59impl From<Account> for User {
60 fn from(row: Account) -> Self {
61 User {
62 id: row.id,
63 username: row.username,
64 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control65 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell66 ..User::default()
Email verification, password reset, and Git for AI scale positioning67 }
68 }
69}
API and MCP server, Rust identity service, registration, site redesign70
71#[derive(Deserialize)]
72struct UserRow {
73 id: String,
74 username: String,
75 password_hash: String,
Email verification, password reset, and Git for AI scale positioning76 verified: u8,
API and MCP server, Rust identity service, registration, site redesign77}
78
Email verification, password reset, and Git for AI scale positioning79/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign80#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning81struct TokenOwner {
82 id: String,
83 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 /// The address a link was sent to; null on links from before accounts
85 /// had several, which are for the primary.
86 #[serde(default)]
87 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning88}
89
90#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign91struct KeyRow {
92 id: String,
93 title: String,
94 fingerprint: String,
RFC 3339 timestamps in identity and repos95 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca96 #[serde(default)]
97 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign98}
99
100impl From<KeyRow> for SshKey {
101 fn from(row: KeyRow) -> Self {
102 SshKey {
103 id: row.id,
104 title: row.title,
105 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos106 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca107 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign108 }
109 }
110}
111
112struct Identity {
113 db: D1Database,
Email verification, password reset, and Git for AI scale positioning114 env: Env,
API and MCP server, Rust identity service, registration, site redesign115}
116
117impl Identity {
Workspaces own repositories118 /// Runs a query that returns at most one user, for showing to others:
119 /// without their workspaces.
120 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning121 Ok(self
122 .db
API and MCP server, Rust identity service, registration, site redesign123 .prepare(sql)
124 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning125 .first::<Account>(None)
126 .await?
127 .map(User::from))
128 }
129
Workspaces own repositories130 /// Attaches the workspaces a user belongs to, so that any service can
131 /// authorize them without asking again.
132 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
133 let Some(mut user) = user else {
134 return Ok(None);
135 };
Merge main (membership, two-factor, GitHub repo roles) into tokens136 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look137 // Roles on single repositories, under the same policy (access.rs).
138 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens139 // Access to a workspace is used only within its policy; see security.rs.
140 let within = self.within_policy(&user.id, memberships, grants).await?;
141 user.workspaces = within.memberships;
142 user.grants = within.grants;
143 user.held = within.held;
Workspaces own repositories144 Ok(Some(user))
145 }
146
147 /// Runs a query that resolves credentials to at most one user.
148 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
149 let user = self.find_public_user(sql, param).await?;
150 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign151 }
152
Email verification, password reset, and Git for AI scale positioning153 /// Consumes a token of `kind`, returning its owner if it was valid.
154 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
155 let id = crypto::sha256_hex(token);
156 let owner = self
157 .db
RFC 3339 timestamps in identity and repos158 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look159 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning160 JOIN users ON users.id = email_tokens.user_id
161 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos162 AND email_tokens.expires_at > {SQL_NOW}"
163 ))
Email verification, password reset, and Git for AI scale positioning164 .bind(&[id.as_str().into(), kind.into()])?
165 .first::<TokenOwner>(None)
166 .await?;
167 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look168 // Every outstanding token of this kind dies with the one used:
169 // every reset link, and every confirmation link for the same
170 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning171 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look172 .prepare(
173 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
174 AND (?2 = 'reset' OR email_id IS ?3)",
175 )
176 .bind(&[
177 owner.id.as_str().into(),
178 kind.into(),
179 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
180 ])?
Email verification, password reset, and Git for AI scale positioning181 .run()
182 .await?;
183 }
184 Ok(owner)
185 }
186
187 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 look188 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
189 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
190 }
191 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning192 }
193
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)194 /// The link in a confirmation email, followed: signed in or not. It
195 /// ends the code sent with it (emails.rs).
196 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<g1t_contracts::accounts::EmailConfirmed>> {
Email verification, password reset, and Git for AI scale positioning197 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
198 return Ok(Outcome::fail(
199 FailureCode::Invalid,
200 "This confirmation link is not valid or has expired.",
201 ));
202 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)203 self.confirm_address(&owner.id, owner.email_id.as_deref()).await
Email verification, password reset, and Git for AI scale positioning204 }
205
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look206 /// Any confirmed address of an account can ask for a reset; so can the
207 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning208 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 look209 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
210 && match a.client.as_deref() {
211 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
212 None => true,
213 };
214 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 exists215 // A failure from here on happens only for a real account, so it
216 // is logged, never answered: the reply below stays the same.
217 if let Err(error) = self.send_reset(&target).await {
218 worker::console_error!("password reset for a known address failed: {error}");
219 }
220 }
221 // The same answer either way, so addresses cannot be probed.
222 Ok(true)
223 }
224
225 /// Saves a reset link for `target` and mails it, telling the account's
226 /// other addresses.
227 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
228 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look229 let token = crypto::random_hex(32);
230 self.db
231 .prepare(format!(
232 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
233 VALUES (?, ?, 'reset', {}, ?)",
234 sql_after(RESET_TTL_SECONDS)
235 ))
236 .bind(&[
237 crypto::sha256_hex(&token).into(),
238 target.user_id.as_str().into(),
239 target.email_id.as_str().into(),
240 ])?
241 .run()
Email verification, password reset, and Git for AI scale positioning242 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look243 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
244 // The primary and the backup hear of it when it went elsewhere.
245 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
246 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
247 let change = format!("A password reset was asked for through {}", target.display);
248 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
249 worker::console_error!("security notice failed: {error}");
250 }
251 }
Email verification, password reset, and Git for AI scale positioning252 }
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 exists253 Ok(())
Email verification, password reset, and Git for AI scale positioning254 }
255
256 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
257 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
258 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
259 }
260 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
261 return Ok(Outcome::fail(
262 FailureCode::Invalid,
263 "This reset link is not valid or has expired.",
264 ));
265 };
266 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look267 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning268 .bind(&[
269 crypto::hash_password(&a.password).into(),
270 owner.id.as_str().into(),
271 ])?
272 .run()
273 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look274 // Following an emailed link also proves the address it went to
275 // (unless another account confirmed it first).
276 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
277 // Anyone signed in with the old password is signed out, and nobody
278 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning279 self.db
280 .prepare("DELETE FROM sessions WHERE user_id = ?")
281 .bind(&[owner.id.as_str().into()])?
282 .run()
283 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
285 self.log_security(&owner.id, "password_changed", None, None).await;
286 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
287 let verified = self
288 .find_public_user(
289 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
290 &owner.id,
291 )
292 .await?
293 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning294 Ok(Outcome::Ok(User {
295 id: owner.id,
296 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look297 verified,
Workspaces own repositories298 ..User::default()
Email verification, password reset, and Git for AI scale positioning299 }))
300 }
301
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look302 /// The account a login names: a username, or any confirmed address.
303 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
304 let login = login.trim().to_lowercase();
305 let (column, value) = if login.contains('@') {
306 match self.user_with_verified_email(&login).await? {
307 Some(id) => ("id", id),
308 None => return Ok(None),
309 }
310 } else {
311 ("username", login)
312 };
313 self.db
314 .prepare(format!(
315 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
316 ))
317 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign318 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 .await
320 }
321
322 /// Checks a password for a login, throttled (see throttle.rs). The
323 /// refusal is one of two messages, the same for every account.
324 async fn checked_password(
325 &self,
326 login: &str,
327 password: &str,
328 client: Option<&str>,
329 ) -> Result<std::result::Result<User, &'static str>> {
330 let row = self.password_row(login).await?;
331 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
332 let (account_key, client_key) = Identity::password_keys(&subject, client);
333 if self.password_locked(&account_key, client_key.as_deref()).await? {
334 return Ok(Err(throttle::THROTTLED));
335 }
336 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
337 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
338 Some(row) => {
339 self.clear(&account_key).await?;
340 Ok(Ok(User {
341 id: row.id,
342 username: row.username,
343 verified: row.verified != 0,
344 ..User::default()
345 }))
346 }
347 None => {
348 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
349 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
350 Ok(Err("Incorrect username or password."))
351 }
352 }
353 }
354
Merge main (membership, two-factor, GitHub repo roles) into tokens355 /// Git over HTTPS with the account's password. With two-factor
356 /// authentication on, a password alone is never enough: use an access
357 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look358 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
359 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens360 if let Some(user) = &user
361 && self.two_factor_enabled(&user.id).await?
362 {
363 return Ok(None);
364 }
Workspaces own repositories365 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign366 }
367
368 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
369 let username = a.username.trim().to_lowercase();
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent370 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign371 let email = a.email.trim().to_lowercase();
372 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 look373 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
374 // The invite first: without one, nothing else on the form matters.
375 if self.invites_required() && invite_code.is_none() {
376 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
377 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent378 if !claimable {
API and MCP server, Rust identity service, registration, site redesign379 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent380 "Usernames use lowercase letters, digits and single hyphens, up to 39 characters, and cannot be a reserved word.",
API and MCP server, Rust identity service, registration, site redesign381 );
382 }
383 let well_formed_email = email
384 .split_once('@')
385 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
386 && !email.contains(char::is_whitespace);
387 if !well_formed_email {
388 return invalid("Enter a valid email address.");
389 }
390 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning391 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign392 }
393 let taken = self
394 .db
Agents as a team: lifecycle, merge queue, billing and a new shell395 // Usernames and workspaces share one namespace, so that a name
396 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look397 // An address is taken once an account has confirmed it; an
398 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell399 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look400 "SELECT username FROM users WHERE username = ?
401 UNION ALL SELECT email FROM user_emails WHERE email = ? AND verified_at IS NOT NULL
Agents as a team: lifecycle, merge queue, billing and a new shell402 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
403 )
404 .bind(&[
405 username.as_str().into(),
406 email.as_str().into(),
407 username.as_str().into(),
408 ])?
API and MCP server, Rust identity service, registration, site redesign409 .first::<serde_json::Value>(None)
410 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look411 // A renamed workspace's old slug stays reserved for it a while, and
412 // a deleted workspace's for good.
413 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign414 return Ok(Outcome::fail(
415 FailureCode::Conflict,
416 "That username or email is already registered.",
417 ));
418 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look419 let password_hash = crypto::hash_password(&a.password);
420 let user = match self
421 .create_account(invites::NewAccount {
422 username: &username,
423 email: &email,
424 password_hash: &password_hash,
425 verified: false,
426 invite_code,
427 client: a.client.as_deref(),
428 })
429 .await?
430 {
431 Outcome::Ok(user) => user,
432 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign433 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)434 // The account exists either way; the email can be sent again from
435 // 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 quotas436 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)437 && 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 quotas438 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)439 worker::console_error!("confirmation email failed: {error}");
Email verification, password reset, and Git for AI scale positioning440 }
API and MCP server, Rust identity service, registration, site redesign441 self.start_session(user).await
442 }
443
444 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 look445 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
446 Ok(user) => user,
447 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign448 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look449 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign450 self.start_session(user).await
451 }
452
Merge main (membership, two-factor, GitHub repo roles) into tokens453 /// Starts a session for someone who just proved their password (or
454 /// GitHub account). With two-factor authentication on, it starts none:
455 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign456 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens457 if self.two_factor_enabled(&user.id).await? {
458 let challenge = self.issue_challenge(&user.id).await?;
459 return Ok(Outcome::Ok(SignedIn {
460 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
461 session_token: String::new(),
462 two_factor_challenge: Some(challenge),
463 }));
464 }
465 self.session_for(user).await
466 }
467
468 /// A new session for `user`, who has proved who they are in full.
469 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
API and MCP server, Rust identity service, registration, site redesign470 let session_token = crypto::random_hex(32);
471 self.db
RFC 3339 timestamps in identity and repos472 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look473 // Signing in is proof it is the person: see security.rs.
474 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos475 sql_after(SESSION_TTL_SECONDS)
476 ))
API and MCP server, Rust identity service, registration, site redesign477 .bind(&[
478 crypto::sha256_hex(&session_token).into(),
479 user.id.as_str().into(),
480 ])?
481 .run()
482 .await?;
483 Ok(Outcome::Ok(SignedIn {
484 user,
485 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens486 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign487 }))
488 }
489
490 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
491 self.db
492 .prepare("DELETE FROM sessions WHERE id = ?")
493 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
494 .run()
495 .await?;
496 Ok(())
497 }
498
499 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
500 self.find_user(
RFC 3339 timestamps in identity and repos501 &format!(
Workspace names and icons, and a component kit for every control502 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
503 users.avatar
RFC 3339 timestamps in identity and repos504 FROM sessions JOIN users ON users.id = sessions.user_id
505 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
506 ),
API and MCP server, Rust identity service, registration, site redesign507 &crypto::sha256_hex(&a.session_token),
508 )
509 .await
510 }
511
512 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
513 // Like GitHub, a token alone identifies its user.
514 if a.secret.starts_with(TOKEN_PREFIX) {
515 self.user_for_access_token(&a.secret).await
516 } else {
517 self.user_for_password(&a.username, &a.secret).await
518 }
519 }
520
521 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
522 self.find_user(
Email verification, password reset, and Git for AI scale positioning523 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified FROM ssh_keys
API and MCP server, Rust identity service, registration, site redesign524 JOIN users ON users.id = ssh_keys.user_id
525 WHERE fingerprint = ?",
526 &a.fingerprint,
527 )
528 .await
529 }
530
531 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories532 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning533 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign534 &a.username.to_lowercase(),
535 )
536 .await
537 }
538
Inbox: threads, reasons, subscriptions and watching539 /// `notify_by_email`: an inbox item, emailed to the person it is for,
540 /// only at a confirmed address and only while they can still read the
541 /// repository it is about. Returns whether it was sent.
542 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
543 #[derive(Deserialize)]
544 struct Address {
545 email: Option<String>,
546 }
547 let user = self
548 .find_user(
549 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
550 &a.username.to_lowercase(),
551 )
552 .await?;
553 let Some(user) = user.filter(|user| user.verified) else {
554 return Ok(false);
555 };
556 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
557 &self.env.service("REPOS")?,
558 "readable",
559 &g1t_contracts::repos::ReadableArgs {
560 ids: vec![a.repo_id.clone()],
561 viewer: Some(user.clone()),
562 },
563 )
564 .await?;
565 if readable.is_empty() {
566 return Ok(false);
567 }
568 let address = self
569 .db
570 .prepare("SELECT email FROM users WHERE id = ?")
571 .bind(&[user.id.as_str().into()])?
572 .first::<Address>(None)
573 .await?
574 .and_then(|row| row.email)
575 .filter(|email| !email.trim().is_empty());
576 let Some(address) = address else {
577 return Ok(false);
578 };
579 email::send_notification(&self.env, &address, &a).await?;
580 Ok(true)
581 }
582
What happened across an outcome, as a feed beside its graph583 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
584 #[derive(serde::Deserialize)]
585 struct Named {
586 id: String,
587 name: String,
588 }
589 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
590 let mut names = std::collections::HashMap::new();
591 if ids.is_empty() {
592 return Ok(names);
593 }
594 let marks = vec!["?"; ids.len()].join(", ");
595 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
596 for sql in [
597 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
598 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
599 ] {
600 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
601 names.insert(row.id, row.name);
602 }
603 }
604 Ok(names)
605 }
606
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97607 /// `accounts`: the accounts behind these ids (at most 200), each with
608 /// its username and avatar, for lists that keep ids, such as who
609 /// starred a repository. Ids of no account are left out.
610 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
611 #[derive(serde::Deserialize)]
612 struct Row {
613 id: String,
614 username: String,
615 avatar: Option<String>,
616 }
617 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
618 let mut found = std::collections::HashMap::new();
619 if ids.is_empty() {
620 return Ok(found);
621 }
622 let marks = vec!["?"; ids.len()].join(", ");
623 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
624 let rows = self
625 .db
626 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
627 .bind(&bind)?
628 .all()
629 .await?
630 .results::<Row>()?;
631 for row in rows {
632 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
633 }
634 Ok(found)
635 }
636
API and MCP server, Rust identity service, registration, site redesign637 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
638 let rows = self
639 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca640 .prepare("SELECT id, title, fingerprint, created_at, last_used_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
API and MCP server, Rust identity service, registration, site redesign641 .bind(&[a.user.id.into()])?
642 .all()
643 .await?
644 .results::<KeyRow>()?;
645 Ok(rows.into_iter().map(SshKey::from).collect())
646 }
647
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge648 /// The account (user id) that registered each key, by fingerprint
649 /// (`SHA256:…`). At most 100; unknown keys are left out.
650 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
651 #[derive(serde::Deserialize)]
652 struct Row {
653 fingerprint: String,
654 user_id: String,
655 }
656 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
657 if fingerprints.is_empty() {
658 return Ok(std::collections::HashMap::new());
659 }
660 let marks = vec!["?"; fingerprints.len()].join(", ");
661 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
662 Ok(self
663 .db
664 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
665 .bind(&binds)?
666 .all()
667 .await?
668 .results::<Row>()?
669 .into_iter()
670 .map(|row| (row.fingerprint, row.user_id))
671 .collect())
672 }
673
API and MCP server, Rust identity service, registration, site redesign674 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
675 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
676 return Ok(Outcome::fail(
677 FailureCode::Invalid,
678 "That is not a valid OpenSSH public key.",
679 ));
680 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca681 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
682 if self.key_in_use(&key.fingerprint).await? {
683 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign684 }
685 let now = now_ms();
686 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
687 .into_iter()
688 .find(|candidate| !candidate.is_empty())
689 .unwrap_or_default()
690 .to_owned();
691 let row = KeyRow {
692 id: new_id("key", now),
693 title,
694 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos695 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca696 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign697 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca698 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign699 .prepare(
700 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
701 VALUES (?, ?, ?, ?, ?, ?)",
702 )
703 .bind(&[
704 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca705 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign706 row.title.as_str().into(),
707 key.public_key.into(),
708 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos709 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign710 ])?
711 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca712 .await;
713 // Added at the same moment elsewhere: the trigger or the unique
714 // index refused it.
715 if let Err(error) = inserted {
716 if self.key_in_use(&row.fingerprint).await? {
717 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
718 }
719 return Err(error);
720 }
Merge main (membership, two-factor, GitHub repo roles) into tokens721 let shown = format!("{} ({})", row.title, row.fingerprint);
722 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
723 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign724 Ok(Outcome::Ok(row.into()))
725 }
726
Merge main (membership, two-factor, GitHub repo roles) into tokens727 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca728 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens729 #[derive(Deserialize)]
730 struct Removed {
731 title: String,
732 fingerprint: String,
733 }
734 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca735 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens736 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca737 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens738 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca739 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens740 if let Some(removed) = removed {
741 let shown = format!("{} ({})", removed.title, removed.fingerprint);
742 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
743 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
744 }
API and MCP server, Rust identity service, registration, site redesign745 Ok(())
746 }
747}
748
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas749/// 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 member750/// the last summary's window was still open, so none waits on a later one;
751/// and deleted workspaces past their restore window are purged
752/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas753#[event(scheduled)]
754async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
755 let Ok(db) = env.d1("DB") else { return };
756 let identity = Identity { db, env };
757 if let Err(error) = identity.notify_staff_of_requests().await {
758 worker::console_error!("waitlist summary: {error}");
759 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member760 if let Err(error) = identity.purge_due_workspaces().await {
761 worker::console_error!("workspace purge: {error}");
762 }
Merge main (membership, two-factor, GitHub repo roles) into tokens763 // Once: creators of repositories made before they got Admin (members.rs).
764 if let Err(error) = identity.backfill_creator_grants().await {
765 worker::console_error!("creator grants: {error}");
766 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas767}
768
API and MCP server, Rust identity service, registration, site redesign769#[event(fetch)]
770async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
771 let Some(method) = rpc_method(&request) else {
772 return Response::error("Not found", 404);
773 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents774 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
775 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign776 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents777 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign778
Fast pages, required checks on the branch, self-hosted runners, honest incidents779 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette780 "register" => {
781 let outcome = identity.register(args(body)?).await?;
782 if let Outcome::Ok(signed_in) = &outcome {
783 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
784 }
785 reply(&outcome)
786 }
API and MCP server, Rust identity service, registration, site redesign787 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette788 "create_workspace" => {
789 let outcome = identity.create_workspace(args(body)?).await?;
790 if let Outcome::Ok(workspace) = &outcome {
791 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
792 }
793 reply(&outcome)
794 }
Workspaces own repositories795 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
796 "list_members" => reply(&identity.list_members(args(body)?).await?),
797 "add_member" => reply(&identity.add_member(args(body)?).await?),
798 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens799 // Owners, roles, leaving and member privileges; see members.rs.
800 "update_member" => reply(&identity.update_member(args(body)?).await?),
801 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
802 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
803 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
804 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
805 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette806 "update_workspace" => {
807 let outcome = identity.update_workspace(args(body)?).await?;
808 if let Outcome::Ok(workspace) = &outcome {
809 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
810 }
811 reply(&outcome)
812 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains813 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
814 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
815 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'816 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look817 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
818 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
819 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette820 "set_workspace_avatar" => {
821 let outcome = identity.set_workspace_avatar(args(body)?).await?;
822 if let Outcome::Ok(workspace) = &outcome {
823 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
824 }
825 reply(&outcome)
826 }
827 "set_user_avatar" => {
828 let a: SetUserAvatarArgs = args(body)?;
829 let (username, id) = (a.user.username.clone(), a.user.id.clone());
830 let outcome = identity.set_user_avatar(a).await?;
831 if matches!(outcome, Outcome::Ok(_)) {
832 identity.announce_user(&username, Some(&id)).await;
833 }
834 reply(&outcome)
835 }
Agents as a team: lifecycle, merge queue, billing and a new shell836 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
837 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
838 "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 look839 // Signing in with GitHub; see github.rs.
840 "github_enabled" => reply(&identity.github_enabled()),
841 "github_start" => reply(&identity.github_start(args(body)?).await?),
842 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
843 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
844 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
845 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
846 "github_account" => reply(&identity.github_account(args(body)?).await?),
847 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
848 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
849 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
850 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications851 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
852 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
853 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
854 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
855 "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 step856 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API857 "device_start" => reply(&identity.device_start(args(body)?).await?),
858 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
859 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
860 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning861 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
862 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)863 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
864 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning865 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
866 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look867 // A person's email addresses; see emails.rs and security.rs.
868 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
869 "add_email" => reply(&identity.add_email(args(body)?).await?),
870 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
871 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
872 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
873 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens874 // Two-factor authentication; see two_factor.rs.
875 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
876 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
877 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
878 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
879 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
880 "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 look881 "security_log" => reply(&identity.security_log(args(body)?).await?),
882 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
883 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
884 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
885 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
886 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign887 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
888 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
889 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
890 "user_for_access_token" => {
891 let a: TokenArgs = args(body)?;
892 reply(&identity.user_for_access_token(&a.token).await?)
893 }
894 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
895 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph896 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97897 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching898 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains899 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette900 "update_profile" => {
901 let outcome = identity.update_profile(args(body)?).await?;
902 if let Outcome::Ok(profile) = &outcome {
903 identity.announce_user(&profile.username, None).await;
904 }
905 reply(&outcome)
906 }
907 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains908 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign909 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge910 // Services only: who registered each key, for verifying commit
911 // signatures (repos' signatures.rs).
912 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign913 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca914 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
915 // A repository's deploy keys, and who an SSH key signs in as; see
916 // deploy_keys.rs.
917 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
918 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
919 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
920 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
921 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign922 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
923 "create_access_token" => reply(&identity.create_access_token(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step924 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity925 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
926 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
927 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
928 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
929 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
930 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
931 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
932 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell933 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
934 "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 API935 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
936 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
937 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'938 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
939 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens940 "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 look941 // Invites and the waitlist; see invites.rs.
942 "registration" => reply(&identity.registration_mode()),
943 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
944 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
945 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
946 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
947 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
948 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
949 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
950 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
951 "request_access" => reply(&identity.request_access(args(body)?).await?),
952 // Who has access to a repository; see access.rs.
953 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
954 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
955 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
956 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
957 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
958 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
959 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
960 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
961 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'962 // Where a workspace keeps its repositories' git data (EU residency).
963 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
964 "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 look965 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca966 "forget_repo_access" => {
967 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
968 // A purged repository's deploy keys go with its access.
969 identity.forget_deploy_keys(&a.repo_id).await?;
970 reply(&identity.forget_repo_access(a).await?)
971 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar972 // Teams (teams.rs).
973 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
974 "get_team" => reply(&identity.get_team(args(body)?).await?),
975 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'976 "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 bar977 "update_team" => reply(&identity.update_team(args(body)?).await?),
978 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
979 "team_members" => reply(&identity.team_members(args(body)?).await?),
980 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
981 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
982 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
983 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
984 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
985 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
986 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
987 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
988 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
989 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace990 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
991 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
992 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
993 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look994 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
995 "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 quotas996 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look997 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
998 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
999 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1000 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1001 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1002 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1003 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1004 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1005 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1006 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1007 // Workspace aliases, set by staff only; see aliases.rs.
1008 "admin_aliases" => reply(&identity.admin_aliases().await?),
1009 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1010 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1011 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1012 };
1013 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1014}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1015
1016#[cfg(test)]
1017mod register_tests {
1018 use super::*;
1019
1020 #[test]
1021 fn nobody_registers_as_g1t() {
1022 // What register checks the username with, whatever its case.
1023 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1024 assert_eq!(claimable_namespace(username), None, "{username}");
1025 }
1026 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1027 }
1028}

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