Skip to content
1,041 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;
Device sign-in replaces registering and minting tokens over the API12mod device;
Search across all of g1t, Explore, and a command palette13mod directory;
Email verification, password reset, and Git for AI scale positioning14mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15mod emails;
16mod github;
17mod invites;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA18mod members;
OAuth 2.1 sign-in for MCP clients and other applications19mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person20mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21mod profiles;
22mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'23mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar26mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell28mod tokens;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA29mod two_factor;
Workspaces own repositories30mod workspaces;
API and MCP server, Rust identity service, registration, site redesign31
32use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos33use 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-agent34use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign35use g1t_kit::{args, now_ms, reply, rpc_method};
36use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell37use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign38use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas39use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign40
RFC 3339 timestamps in identity and repos41const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
42const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
43const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign44const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning45const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
46
47/// A user as selected from the database; `verified` arrives as 0 or 1.
48#[derive(Deserialize)]
49struct Account {
50 id: String,
51 username: String,
52 verified: u8,
Workspace names and icons, and a component kit for every control53 /// Selected only where the person is being shown to themselves.
54 #[serde(default)]
55 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning56}
57
58impl From<Account> for User {
59 fn from(row: Account) -> Self {
60 User {
61 id: row.id,
62 username: row.username,
63 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control64 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell65 ..User::default()
Email verification, password reset, and Git for AI scale positioning66 }
67 }
68}
API and MCP server, Rust identity service, registration, site redesign69
70#[derive(Deserialize)]
71struct UserRow {
72 id: String,
73 username: String,
74 password_hash: String,
Email verification, password reset, and Git for AI scale positioning75 verified: u8,
API and MCP server, Rust identity service, registration, site redesign76}
77
Email verification, password reset, and Git for AI scale positioning78/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign79#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning80struct TokenOwner {
81 id: String,
82 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look83 /// The address a link was sent to; null on links from before accounts
84 /// had several, which are for the primary.
85 #[serde(default)]
86 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning87}
88
89#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign90struct KeyRow {
91 id: String,
92 title: String,
93 fingerprint: String,
RFC 3339 timestamps in identity and repos94 created_at: String,
API and MCP server, Rust identity service, registration, site redesign95}
96
97impl From<KeyRow> for SshKey {
98 fn from(row: KeyRow) -> Self {
99 SshKey {
100 id: row.id,
101 title: row.title,
102 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos103 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign104 }
105 }
106}
107
108struct Identity {
109 db: D1Database,
Email verification, password reset, and Git for AI scale positioning110 env: Env,
API and MCP server, Rust identity service, registration, site redesign111}
112
113impl Identity {
Workspaces own repositories114 /// Runs a query that returns at most one user, for showing to others:
115 /// without their workspaces.
116 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning117 Ok(self
118 .db
API and MCP server, Rust identity service, registration, site redesign119 .prepare(sql)
120 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning121 .first::<Account>(None)
122 .await?
123 .map(User::from))
124 }
125
Workspaces own repositories126 /// Attaches the workspaces a user belongs to, so that any service can
127 /// authorize them without asking again.
128 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
129 let Some(mut user) = user else {
130 return Ok(None);
131 };
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA132 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look133 // Roles on single repositories, under the same policy (access.rs).
134 let grants = self.grants_of(&user.id).await?;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA135 // Access to a workspace is used only within its policy; see security.rs.
136 let within = self.within_policy(&user.id, memberships, grants).await?;
137 user.workspaces = within.memberships;
138 user.grants = within.grants;
139 user.held = within.held;
Workspaces own repositories140 Ok(Some(user))
141 }
142
143 /// Runs a query that resolves credentials to at most one user.
144 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
145 let user = self.find_public_user(sql, param).await?;
146 self.with_workspaces(user).await
147 }
148
Email verification, password reset, and Git for AI scale positioning149 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos150 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning151 let token = crypto::random_hex(32);
152 self.db
RFC 3339 timestamps in identity and repos153 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning154 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos155 VALUES (?, ?, ?, {})",
156 sql_after(ttl)
157 ))
Email verification, password reset, and Git for AI scale positioning158 .bind(&[
159 crypto::sha256_hex(&token).into(),
160 user_id.into(),
161 kind.into(),
162 ])?
163 .run()
164 .await?;
165 Ok(token)
API and MCP server, Rust identity service, registration, site redesign166 }
167
Email verification, password reset, and Git for AI scale positioning168 /// Consumes a token of `kind`, returning its owner if it was valid.
169 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
170 let id = crypto::sha256_hex(token);
171 let owner = self
172 .db
RFC 3339 timestamps in identity and repos173 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look174 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning175 JOIN users ON users.id = email_tokens.user_id
176 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos177 AND email_tokens.expires_at > {SQL_NOW}"
178 ))
Email verification, password reset, and Git for AI scale positioning179 .bind(&[id.as_str().into(), kind.into()])?
180 .first::<TokenOwner>(None)
181 .await?;
182 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look183 // Every outstanding token of this kind dies with the one used:
184 // every reset link, and every confirmation link for the same
185 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning186 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look187 .prepare(
188 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
189 AND (?2 = 'reset' OR email_id IS ?3)",
190 )
191 .bind(&[
192 owner.id.as_str().into(),
193 kind.into(),
194 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
195 ])?
Email verification, password reset, and Git for AI scale positioning196 .run()
197 .await?;
198 }
199 Ok(owner)
200 }
201
202 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
203 let token = self
204 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
205 .await?;
206 email::send_verification(&self.env, email, &user.username, &token).await
207 }
208
209 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 look210 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
211 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
212 }
213 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning214 }
215
216 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
217 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
218 return Ok(Outcome::fail(
219 FailureCode::Invalid,
220 "This confirmation link is not valid or has expired.",
221 ));
222 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look223 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
224 return Ok(Outcome::Fail(failure));
225 }
226 // Whether the account is confirmed: whether its primary is.
227 let verified = self
228 .find_public_user(
229 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
230 &owner.id,
231 )
232 .await?
233 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning234 Ok(Outcome::Ok(User {
235 id: owner.id,
236 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 verified,
Workspaces own repositories238 ..User::default()
Email verification, password reset, and Git for AI scale positioning239 }))
240 }
241
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 /// Any confirmed address of an account can ask for a reset; so can the
243 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning244 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 look245 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
246 && match a.client.as_deref() {
247 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
248 None => true,
249 };
250 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 exists251 // A failure from here on happens only for a real account, so it
252 // is logged, never answered: the reply below stays the same.
253 if let Err(error) = self.send_reset(&target).await {
254 worker::console_error!("password reset for a known address failed: {error}");
255 }
256 }
257 // The same answer either way, so addresses cannot be probed.
258 Ok(true)
259 }
260
261 /// Saves a reset link for `target` and mails it, telling the account's
262 /// other addresses.
263 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
264 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look265 let token = crypto::random_hex(32);
266 self.db
267 .prepare(format!(
268 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
269 VALUES (?, ?, 'reset', {}, ?)",
270 sql_after(RESET_TTL_SECONDS)
271 ))
272 .bind(&[
273 crypto::sha256_hex(&token).into(),
274 target.user_id.as_str().into(),
275 target.email_id.as_str().into(),
276 ])?
277 .run()
Email verification, password reset, and Git for AI scale positioning278 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
280 // The primary and the backup hear of it when it went elsewhere.
281 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
282 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
283 let change = format!("A password reset was asked for through {}", target.display);
284 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
285 worker::console_error!("security notice failed: {error}");
286 }
287 }
Email verification, password reset, and Git for AI scale positioning288 }
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 exists289 Ok(())
Email verification, password reset, and Git for AI scale positioning290 }
291
292 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
293 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
294 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
295 }
296 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
297 return Ok(Outcome::fail(
298 FailureCode::Invalid,
299 "This reset link is not valid or has expired.",
300 ));
301 };
302 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look303 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning304 .bind(&[
305 crypto::hash_password(&a.password).into(),
306 owner.id.as_str().into(),
307 ])?
308 .run()
309 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310 // Following an emailed link also proves the address it went to
311 // (unless another account confirmed it first).
312 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
313 // Anyone signed in with the old password is signed out, and nobody
314 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning315 self.db
316 .prepare("DELETE FROM sessions WHERE user_id = ?")
317 .bind(&[owner.id.as_str().into()])?
318 .run()
319 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look320 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
321 self.log_security(&owner.id, "password_changed", None, None).await;
322 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
323 let verified = self
324 .find_public_user(
325 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
326 &owner.id,
327 )
328 .await?
329 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning330 Ok(Outcome::Ok(User {
331 id: owner.id,
332 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 verified,
Workspaces own repositories334 ..User::default()
Email verification, password reset, and Git for AI scale positioning335 }))
336 }
337
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 /// The account a login names: a username, or any confirmed address.
339 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
340 let login = login.trim().to_lowercase();
341 let (column, value) = if login.contains('@') {
342 match self.user_with_verified_email(&login).await? {
343 Some(id) => ("id", id),
344 None => return Ok(None),
345 }
346 } else {
347 ("username", login)
348 };
349 self.db
350 .prepare(format!(
351 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
352 ))
353 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign354 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look355 .await
356 }
357
358 /// Checks a password for a login, throttled (see throttle.rs). The
359 /// refusal is one of two messages, the same for every account.
360 async fn checked_password(
361 &self,
362 login: &str,
363 password: &str,
364 client: Option<&str>,
365 ) -> Result<std::result::Result<User, &'static str>> {
366 let row = self.password_row(login).await?;
367 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
368 let (account_key, client_key) = Identity::password_keys(&subject, client);
369 if self.password_locked(&account_key, client_key.as_deref()).await? {
370 return Ok(Err(throttle::THROTTLED));
371 }
372 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
373 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
374 Some(row) => {
375 self.clear(&account_key).await?;
376 Ok(Ok(User {
377 id: row.id,
378 username: row.username,
379 verified: row.verified != 0,
380 ..User::default()
381 }))
382 }
383 None => {
384 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
385 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
386 Ok(Err("Incorrect username or password."))
387 }
388 }
389 }
390
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA391 /// Git over HTTPS with the account's password. With two-factor
392 /// authentication on, a password alone is never enough: use an access
393 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look394 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
395 let user = self.checked_password(login, password, None).await?.ok();
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA396 if let Some(user) = &user
397 && self.two_factor_enabled(&user.id).await?
398 {
399 return Ok(None);
400 }
Workspaces own repositories401 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign402 }
403
404 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
405 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-agent406 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign407 let email = a.email.trim().to_lowercase();
408 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 look409 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
410 // The invite first: without one, nothing else on the form matters.
411 if self.invites_required() && invite_code.is_none() {
412 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
413 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent414 if !claimable {
API and MCP server, Rust identity service, registration, site redesign415 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent416 "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 redesign417 );
418 }
419 let well_formed_email = email
420 .split_once('@')
421 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
422 && !email.contains(char::is_whitespace);
423 if !well_formed_email {
424 return invalid("Enter a valid email address.");
425 }
426 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning427 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign428 }
429 let taken = self
430 .db
Agents as a team: lifecycle, merge queue, billing and a new shell431 // Usernames and workspaces share one namespace, so that a name
432 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look433 // An address is taken once an account has confirmed it; an
434 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell435 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look436 "SELECT username FROM users WHERE username = ?
437 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 shell438 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
439 )
440 .bind(&[
441 username.as_str().into(),
442 email.as_str().into(),
443 username.as_str().into(),
444 ])?
API and MCP server, Rust identity service, registration, site redesign445 .first::<serde_json::Value>(None)
446 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look447 // A renamed workspace's old slug stays reserved for it a while, and
448 // a deleted workspace's for good.
449 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign450 return Ok(Outcome::fail(
451 FailureCode::Conflict,
452 "That username or email is already registered.",
453 ));
454 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look455 let password_hash = crypto::hash_password(&a.password);
456 let user = match self
457 .create_account(invites::NewAccount {
458 username: &username,
459 email: &email,
460 password_hash: &password_hash,
461 verified: false,
462 invite_code,
463 client: a.client.as_deref(),
464 })
465 .await?
466 {
467 Outcome::Ok(user) => user,
468 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign469 };
Email verification, password reset, and Git for AI scale positioning470 // The account exists either way; the email can be sent again later.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas471 // An invite sent to this address confirmed it already (invites.rs).
472 if !user.verified
473 && let Err(error) = self.send_verification(&user, &email).await
474 {
Email verification, password reset, and Git for AI scale positioning475 worker::console_error!("verification email failed: {error}");
476 }
API and MCP server, Rust identity service, registration, site redesign477 self.start_session(user).await
478 }
479
480 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 look481 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
482 Ok(user) => user,
483 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign484 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look485 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign486 self.start_session(user).await
487 }
488
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA489 /// Starts a session for someone who just proved their password (or
490 /// GitHub account). With two-factor authentication on, it starts none:
491 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign492 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA493 if self.two_factor_enabled(&user.id).await? {
494 let challenge = self.issue_challenge(&user.id).await?;
495 return Ok(Outcome::Ok(SignedIn {
496 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
497 session_token: String::new(),
498 two_factor_challenge: Some(challenge),
499 }));
500 }
501 self.session_for(user).await
502 }
503
504 /// A new session for `user`, who has proved who they are in full.
505 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
API and MCP server, Rust identity service, registration, site redesign506 let session_token = crypto::random_hex(32);
507 self.db
RFC 3339 timestamps in identity and repos508 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look509 // Signing in is proof it is the person: see security.rs.
510 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos511 sql_after(SESSION_TTL_SECONDS)
512 ))
API and MCP server, Rust identity service, registration, site redesign513 .bind(&[
514 crypto::sha256_hex(&session_token).into(),
515 user.id.as_str().into(),
516 ])?
517 .run()
518 .await?;
519 Ok(Outcome::Ok(SignedIn {
520 user,
521 session_token,
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA522 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign523 }))
524 }
525
526 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
527 self.db
528 .prepare("DELETE FROM sessions WHERE id = ?")
529 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
530 .run()
531 .await?;
532 Ok(())
533 }
534
535 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
536 self.find_user(
RFC 3339 timestamps in identity and repos537 &format!(
Workspace names and icons, and a component kit for every control538 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
539 users.avatar
RFC 3339 timestamps in identity and repos540 FROM sessions JOIN users ON users.id = sessions.user_id
541 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
542 ),
API and MCP server, Rust identity service, registration, site redesign543 &crypto::sha256_hex(&a.session_token),
544 )
545 .await
546 }
547
548 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
549 // Like GitHub, a token alone identifies its user.
550 if a.secret.starts_with(TOKEN_PREFIX) {
551 self.user_for_access_token(&a.secret).await
552 } else {
553 self.user_for_password(&a.username, &a.secret).await
554 }
555 }
556
557 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
558 self.find_user(
Email verification, password reset, and Git for AI scale positioning559 "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 redesign560 JOIN users ON users.id = ssh_keys.user_id
561 WHERE fingerprint = ?",
562 &a.fingerprint,
563 )
564 .await
565 }
566
567 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories568 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning569 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign570 &a.username.to_lowercase(),
571 )
572 .await
573 }
574
Inbox: threads, reasons, subscriptions and watching575 /// `notify_by_email`: an inbox item, emailed to the person it is for,
576 /// only at a confirmed address and only while they can still read the
577 /// repository it is about. Returns whether it was sent.
578 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
579 #[derive(Deserialize)]
580 struct Address {
581 email: Option<String>,
582 }
583 let user = self
584 .find_user(
585 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
586 &a.username.to_lowercase(),
587 )
588 .await?;
589 let Some(user) = user.filter(|user| user.verified) else {
590 return Ok(false);
591 };
592 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
593 &self.env.service("REPOS")?,
594 "readable",
595 &g1t_contracts::repos::ReadableArgs {
596 ids: vec![a.repo_id.clone()],
597 viewer: Some(user.clone()),
598 },
599 )
600 .await?;
601 if readable.is_empty() {
602 return Ok(false);
603 }
604 let address = self
605 .db
606 .prepare("SELECT email FROM users WHERE id = ?")
607 .bind(&[user.id.as_str().into()])?
608 .first::<Address>(None)
609 .await?
610 .and_then(|row| row.email)
611 .filter(|email| !email.trim().is_empty());
612 let Some(address) = address else {
613 return Ok(false);
614 };
615 email::send_notification(&self.env, &address, &a).await?;
616 Ok(true)
617 }
618
What happened across an outcome, as a feed beside its graph619 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
620 #[derive(serde::Deserialize)]
621 struct Named {
622 id: String,
623 name: String,
624 }
625 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
626 let mut names = std::collections::HashMap::new();
627 if ids.is_empty() {
628 return Ok(names);
629 }
630 let marks = vec!["?"; ids.len()].join(", ");
631 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
632 for sql in [
633 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
634 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
635 ] {
636 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
637 names.insert(row.id, row.name);
638 }
639 }
640 Ok(names)
641 }
642
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97643 /// `accounts`: the accounts behind these ids (at most 200), each with
644 /// its username and avatar, for lists that keep ids, such as who
645 /// starred a repository. Ids of no account are left out.
646 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
647 #[derive(serde::Deserialize)]
648 struct Row {
649 id: String,
650 username: String,
651 avatar: Option<String>,
652 }
653 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
654 let mut found = std::collections::HashMap::new();
655 if ids.is_empty() {
656 return Ok(found);
657 }
658 let marks = vec!["?"; ids.len()].join(", ");
659 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
660 let rows = self
661 .db
662 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
663 .bind(&bind)?
664 .all()
665 .await?
666 .results::<Row>()?;
667 for row in rows {
668 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
669 }
670 Ok(found)
671 }
672
API and MCP server, Rust identity service, registration, site redesign673 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
674 let rows = self
675 .db
676 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
677 .bind(&[a.user.id.into()])?
678 .all()
679 .await?
680 .results::<KeyRow>()?;
681 Ok(rows.into_iter().map(SshKey::from).collect())
682 }
683
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge684 /// The account (user id) that registered each key, by fingerprint
685 /// (`SHA256:…`). At most 100; unknown keys are left out.
686 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
687 #[derive(serde::Deserialize)]
688 struct Row {
689 fingerprint: String,
690 user_id: String,
691 }
692 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
693 if fingerprints.is_empty() {
694 return Ok(std::collections::HashMap::new());
695 }
696 let marks = vec!["?"; fingerprints.len()].join(", ");
697 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
698 Ok(self
699 .db
700 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
701 .bind(&binds)?
702 .all()
703 .await?
704 .results::<Row>()?
705 .into_iter()
706 .map(|row| (row.fingerprint, row.user_id))
707 .collect())
708 }
709
API and MCP server, Rust identity service, registration, site redesign710 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
711 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
712 return Ok(Outcome::fail(
713 FailureCode::Invalid,
714 "That is not a valid OpenSSH public key.",
715 ));
716 };
717 let taken = self
718 .db
719 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
720 .bind(&[key.fingerprint.as_str().into()])?
721 .first::<serde_json::Value>(None)
722 .await?;
723 if taken.is_some() {
724 return Ok(Outcome::fail(
725 FailureCode::Conflict,
726 "That key is already registered.",
727 ));
728 }
729 let now = now_ms();
730 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
731 .into_iter()
732 .find(|candidate| !candidate.is_empty())
733 .unwrap_or_default()
734 .to_owned();
735 let row = KeyRow {
736 id: new_id("key", now),
737 title,
738 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos739 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign740 };
741 self.db
742 .prepare(
743 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
744 VALUES (?, ?, ?, ?, ?, ?)",
745 )
746 .bind(&[
747 row.id.as_str().into(),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA748 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign749 row.title.as_str().into(),
750 key.public_key.into(),
751 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos752 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign753 ])?
754 .run()
755 .await?;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA756 let shown = format!("{} ({})", row.title, row.fingerprint);
757 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
758 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign759 Ok(Outcome::Ok(row.into()))
760 }
761
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA762 /// Deletes one of the person's SSH keys.
763 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
764 #[derive(Deserialize)]
765 struct Removed {
766 title: String,
767 fingerprint: String,
768 }
769 let removed = self
770 .db
771 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
772 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
773 .first::<Removed>(None)
API and MCP server, Rust identity service, registration, site redesign774 .await?;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA775 if let Some(removed) = removed {
776 let shown = format!("{} ({})", removed.title, removed.fingerprint);
777 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
778 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
779 }
API and MCP server, Rust identity service, registration, site redesign780 Ok(())
781 }
782}
783
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas784/// 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 member785/// the last summary's window was still open, so none waits on a later one;
786/// and deleted workspaces past their restore window are purged
787/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas788#[event(scheduled)]
789async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
790 let Ok(db) = env.d1("DB") else { return };
791 let identity = Identity { db, env };
792 if let Err(error) = identity.notify_staff_of_requests().await {
793 worker::console_error!("waitlist summary: {error}");
794 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member795 if let Err(error) = identity.purge_due_workspaces().await {
796 worker::console_error!("workspace purge: {error}");
797 }
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA798 // Once: creators of repositories made before they got Admin (members.rs).
799 if let Err(error) = identity.backfill_creator_grants().await {
800 worker::console_error!("creator grants: {error}");
801 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas802}
803
API and MCP server, Rust identity service, registration, site redesign804#[event(fetch)]
805async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
806 let Some(method) = rpc_method(&request) else {
807 return Response::error("Not found", 404);
808 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents809 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
810 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign811 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents812 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign813
Fast pages, required checks on the branch, self-hosted runners, honest incidents814 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette815 "register" => {
816 let outcome = identity.register(args(body)?).await?;
817 if let Outcome::Ok(signed_in) = &outcome {
818 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
819 }
820 reply(&outcome)
821 }
API and MCP server, Rust identity service, registration, site redesign822 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette823 "create_workspace" => {
824 let outcome = identity.create_workspace(args(body)?).await?;
825 if let Outcome::Ok(workspace) = &outcome {
826 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
827 }
828 reply(&outcome)
829 }
Workspaces own repositories830 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
831 "list_members" => reply(&identity.list_members(args(body)?).await?),
832 "add_member" => reply(&identity.add_member(args(body)?).await?),
833 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA834 // Owners, roles, leaving and member privileges; see members.rs.
835 "update_member" => reply(&identity.update_member(args(body)?).await?),
836 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
837 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
838 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
839 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
840 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette841 "update_workspace" => {
842 let outcome = identity.update_workspace(args(body)?).await?;
843 if let Outcome::Ok(workspace) = &outcome {
844 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
845 }
846 reply(&outcome)
847 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains848 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
849 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
850 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'851 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look852 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
853 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
854 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette855 "set_workspace_avatar" => {
856 let outcome = identity.set_workspace_avatar(args(body)?).await?;
857 if let Outcome::Ok(workspace) = &outcome {
858 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
859 }
860 reply(&outcome)
861 }
862 "set_user_avatar" => {
863 let a: SetUserAvatarArgs = args(body)?;
864 let (username, id) = (a.user.username.clone(), a.user.id.clone());
865 let outcome = identity.set_user_avatar(a).await?;
866 if matches!(outcome, Outcome::Ok(_)) {
867 identity.announce_user(&username, Some(&id)).await;
868 }
869 reply(&outcome)
870 }
Agents as a team: lifecycle, merge queue, billing and a new shell871 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
872 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
873 "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 look874 // Signing in with GitHub; see github.rs.
875 "github_enabled" => reply(&identity.github_enabled()),
876 "github_start" => reply(&identity.github_start(args(body)?).await?),
877 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
878 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
879 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
880 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
881 "github_account" => reply(&identity.github_account(args(body)?).await?),
882 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
883 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
884 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
885 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications886 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
887 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
888 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
889 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
890 "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 step891 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API892 "device_start" => reply(&identity.device_start(args(body)?).await?),
893 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
894 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
895 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning896 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
897 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
898 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
899 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look900 // A person's email addresses; see emails.rs and security.rs.
901 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
902 "add_email" => reply(&identity.add_email(args(body)?).await?),
903 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
904 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
905 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
906 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA907 // Two-factor authentication; see two_factor.rs.
908 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
909 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
910 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
911 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
912 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
913 "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 look914 "security_log" => reply(&identity.security_log(args(body)?).await?),
915 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
916 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
917 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
918 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
919 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign920 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
921 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
922 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
923 "user_for_access_token" => {
924 let a: TokenArgs = args(body)?;
925 reply(&identity.user_for_access_token(&a.token).await?)
926 }
927 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
928 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph929 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97930 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching931 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains932 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette933 "update_profile" => {
934 let outcome = identity.update_profile(args(body)?).await?;
935 if let Outcome::Ok(profile) = &outcome {
936 identity.announce_user(&profile.username, None).await;
937 }
938 reply(&outcome)
939 }
940 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains941 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign942 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge943 // Services only: who registered each key, for verifying commit
944 // signatures (repos' signatures.rs).
945 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign946 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA947 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign948 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
949 "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 step950 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell951 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
952 "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 API953 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
954 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
955 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'956 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
957 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA958 "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 look959 // Invites and the waitlist; see invites.rs.
960 "registration" => reply(&identity.registration_mode()),
961 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
962 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
963 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
964 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
965 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
966 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
967 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
968 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
969 "request_access" => reply(&identity.request_access(args(body)?).await?),
970 // Who has access to a repository; see access.rs.
971 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
972 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
973 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
974 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
975 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
976 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
977 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
978 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
979 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'980 // Where a workspace keeps its repositories' git data (EU residency).
981 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
982 "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 look983 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
984 "forget_repo_access" => reply(&identity.forget_repo_access(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar985 // Teams (teams.rs).
986 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
987 "get_team" => reply(&identity.get_team(args(body)?).await?),
988 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'989 "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 bar990 "update_team" => reply(&identity.update_team(args(body)?).await?),
991 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
992 "team_members" => reply(&identity.team_members(args(body)?).await?),
993 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
994 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
995 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
996 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
997 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
998 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
999 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1000 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1001 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1002 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1003 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1004 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1005 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1006 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1007 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1008 "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 quotas1009 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1010 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1011 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1012 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1013 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1014 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1015 "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 member1016 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1017 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1018 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1019 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1020 // Workspace aliases, set by staff only; see aliases.rs.
1021 "admin_aliases" => reply(&identity.admin_aliases().await?),
1022 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1023 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1024 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1025 };
1026 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1027}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1028
1029#[cfg(test)]
1030mod register_tests {
1031 use super::*;
1032
1033 #[test]
1034 fn nobody_registers_as_g1t() {
1035 // What register checks the username with, whatever its case.
1036 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1037 assert_eq!(claimable_namespace(username), None, "{username}");
1038 }
1039 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1040 }
1041}

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