Skip to content
1,067 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 VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
45const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign46const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning47const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
48
49/// A user as selected from the database; `verified` arrives as 0 or 1.
50#[derive(Deserialize)]
51struct Account {
52 id: String,
53 username: String,
54 verified: u8,
Workspace names and icons, and a component kit for every control55 /// Selected only where the person is being shown to themselves.
56 #[serde(default)]
57 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning58}
59
60impl From<Account> for User {
61 fn from(row: Account) -> Self {
62 User {
63 id: row.id,
64 username: row.username,
65 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control66 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell67 ..User::default()
Email verification, password reset, and Git for AI scale positioning68 }
69 }
70}
API and MCP server, Rust identity service, registration, site redesign71
72#[derive(Deserialize)]
73struct UserRow {
74 id: String,
75 username: String,
76 password_hash: String,
Email verification, password reset, and Git for AI scale positioning77 verified: u8,
API and MCP server, Rust identity service, registration, site redesign78}
79
Email verification, password reset, and Git for AI scale positioning80/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign81#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning82struct TokenOwner {
83 id: String,
84 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 /// The address a link was sent to; null on links from before accounts
86 /// had several, which are for the primary.
87 #[serde(default)]
88 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning89}
90
91#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign92struct KeyRow {
93 id: String,
94 title: String,
95 fingerprint: String,
RFC 3339 timestamps in identity and repos96 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca97 #[serde(default)]
98 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign99}
100
101impl From<KeyRow> for SshKey {
102 fn from(row: KeyRow) -> Self {
103 SshKey {
104 id: row.id,
105 title: row.title,
106 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos107 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca108 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign109 }
110 }
111}
112
113struct Identity {
114 db: D1Database,
Email verification, password reset, and Git for AI scale positioning115 env: Env,
API and MCP server, Rust identity service, registration, site redesign116}
117
118impl Identity {
Workspaces own repositories119 /// Runs a query that returns at most one user, for showing to others:
120 /// without their workspaces.
121 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning122 Ok(self
123 .db
API and MCP server, Rust identity service, registration, site redesign124 .prepare(sql)
125 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning126 .first::<Account>(None)
127 .await?
128 .map(User::from))
129 }
130
Workspaces own repositories131 /// Attaches the workspaces a user belongs to, so that any service can
132 /// authorize them without asking again.
133 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
134 let Some(mut user) = user else {
135 return Ok(None);
136 };
Merge main (membership, two-factor, GitHub repo roles) into tokens137 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look138 // Roles on single repositories, under the same policy (access.rs).
139 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens140 // Access to a workspace is used only within its policy; see security.rs.
141 let within = self.within_policy(&user.id, memberships, grants).await?;
142 user.workspaces = within.memberships;
143 user.grants = within.grants;
144 user.held = within.held;
Workspaces own repositories145 Ok(Some(user))
146 }
147
148 /// Runs a query that resolves credentials to at most one user.
149 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
150 let user = self.find_public_user(sql, param).await?;
151 self.with_workspaces(user).await
152 }
153
Email verification, password reset, and Git for AI scale positioning154 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos155 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning156 let token = crypto::random_hex(32);
157 self.db
RFC 3339 timestamps in identity and repos158 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning159 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos160 VALUES (?, ?, ?, {})",
161 sql_after(ttl)
162 ))
Email verification, password reset, and Git for AI scale positioning163 .bind(&[
164 crypto::sha256_hex(&token).into(),
165 user_id.into(),
166 kind.into(),
167 ])?
168 .run()
169 .await?;
170 Ok(token)
API and MCP server, Rust identity service, registration, site redesign171 }
172
Email verification, password reset, and Git for AI scale positioning173 /// Consumes a token of `kind`, returning its owner if it was valid.
174 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
175 let id = crypto::sha256_hex(token);
176 let owner = self
177 .db
RFC 3339 timestamps in identity and repos178 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look179 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning180 JOIN users ON users.id = email_tokens.user_id
181 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos182 AND email_tokens.expires_at > {SQL_NOW}"
183 ))
Email verification, password reset, and Git for AI scale positioning184 .bind(&[id.as_str().into(), kind.into()])?
185 .first::<TokenOwner>(None)
186 .await?;
187 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look188 // Every outstanding token of this kind dies with the one used:
189 // every reset link, and every confirmation link for the same
190 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning191 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look192 .prepare(
193 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
194 AND (?2 = 'reset' OR email_id IS ?3)",
195 )
196 .bind(&[
197 owner.id.as_str().into(),
198 kind.into(),
199 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
200 ])?
Email verification, password reset, and Git for AI scale positioning201 .run()
202 .await?;
203 }
204 Ok(owner)
205 }
206
207 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
208 let token = self
209 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
210 .await?;
211 email::send_verification(&self.env, email, &user.username, &token).await
212 }
213
214 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 look215 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
216 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
217 }
218 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning219 }
220
221 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
222 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
223 return Ok(Outcome::fail(
224 FailureCode::Invalid,
225 "This confirmation link is not valid or has expired.",
226 ));
227 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look228 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
229 return Ok(Outcome::Fail(failure));
230 }
231 // Whether the account is confirmed: whether its primary is.
232 let verified = self
233 .find_public_user(
234 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
235 &owner.id,
236 )
237 .await?
238 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning239 Ok(Outcome::Ok(User {
240 id: owner.id,
241 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 verified,
Workspaces own repositories243 ..User::default()
Email verification, password reset, and Git for AI scale positioning244 }))
245 }
246
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look247 /// Any confirmed address of an account can ask for a reset; so can the
248 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning249 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 look250 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
251 && match a.client.as_deref() {
252 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
253 None => true,
254 };
255 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 exists256 // A failure from here on happens only for a real account, so it
257 // is logged, never answered: the reply below stays the same.
258 if let Err(error) = self.send_reset(&target).await {
259 worker::console_error!("password reset for a known address failed: {error}");
260 }
261 }
262 // The same answer either way, so addresses cannot be probed.
263 Ok(true)
264 }
265
266 /// Saves a reset link for `target` and mails it, telling the account's
267 /// other addresses.
268 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
269 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look270 let token = crypto::random_hex(32);
271 self.db
272 .prepare(format!(
273 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
274 VALUES (?, ?, 'reset', {}, ?)",
275 sql_after(RESET_TTL_SECONDS)
276 ))
277 .bind(&[
278 crypto::sha256_hex(&token).into(),
279 target.user_id.as_str().into(),
280 target.email_id.as_str().into(),
281 ])?
282 .run()
Email verification, password reset, and Git for AI scale positioning283 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
285 // The primary and the backup hear of it when it went elsewhere.
286 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
287 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
288 let change = format!("A password reset was asked for through {}", target.display);
289 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
290 worker::console_error!("security notice failed: {error}");
291 }
292 }
Email verification, password reset, and Git for AI scale positioning293 }
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 exists294 Ok(())
Email verification, password reset, and Git for AI scale positioning295 }
296
297 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
298 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
299 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
300 }
301 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
302 return Ok(Outcome::fail(
303 FailureCode::Invalid,
304 "This reset link is not valid or has expired.",
305 ));
306 };
307 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look308 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning309 .bind(&[
310 crypto::hash_password(&a.password).into(),
311 owner.id.as_str().into(),
312 ])?
313 .run()
314 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look315 // Following an emailed link also proves the address it went to
316 // (unless another account confirmed it first).
317 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
318 // Anyone signed in with the old password is signed out, and nobody
319 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning320 self.db
321 .prepare("DELETE FROM sessions WHERE user_id = ?")
322 .bind(&[owner.id.as_str().into()])?
323 .run()
324 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look325 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
326 self.log_security(&owner.id, "password_changed", None, None).await;
327 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
328 let verified = self
329 .find_public_user(
330 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
331 &owner.id,
332 )
333 .await?
334 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning335 Ok(Outcome::Ok(User {
336 id: owner.id,
337 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 verified,
Workspaces own repositories339 ..User::default()
Email verification, password reset, and Git for AI scale positioning340 }))
341 }
342
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look343 /// The account a login names: a username, or any confirmed address.
344 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
345 let login = login.trim().to_lowercase();
346 let (column, value) = if login.contains('@') {
347 match self.user_with_verified_email(&login).await? {
348 Some(id) => ("id", id),
349 None => return Ok(None),
350 }
351 } else {
352 ("username", login)
353 };
354 self.db
355 .prepare(format!(
356 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
357 ))
358 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign359 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 .await
361 }
362
363 /// Checks a password for a login, throttled (see throttle.rs). The
364 /// refusal is one of two messages, the same for every account.
365 async fn checked_password(
366 &self,
367 login: &str,
368 password: &str,
369 client: Option<&str>,
370 ) -> Result<std::result::Result<User, &'static str>> {
371 let row = self.password_row(login).await?;
372 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
373 let (account_key, client_key) = Identity::password_keys(&subject, client);
374 if self.password_locked(&account_key, client_key.as_deref()).await? {
375 return Ok(Err(throttle::THROTTLED));
376 }
377 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
378 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
379 Some(row) => {
380 self.clear(&account_key).await?;
381 Ok(Ok(User {
382 id: row.id,
383 username: row.username,
384 verified: row.verified != 0,
385 ..User::default()
386 }))
387 }
388 None => {
389 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
390 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
391 Ok(Err("Incorrect username or password."))
392 }
393 }
394 }
395
Merge main (membership, two-factor, GitHub repo roles) into tokens396 /// Git over HTTPS with the account's password. With two-factor
397 /// authentication on, a password alone is never enough: use an access
398 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look399 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
400 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens401 if let Some(user) = &user
402 && self.two_factor_enabled(&user.id).await?
403 {
404 return Ok(None);
405 }
Workspaces own repositories406 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign407 }
408
409 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
410 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-agent411 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign412 let email = a.email.trim().to_lowercase();
413 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 look414 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
415 // The invite first: without one, nothing else on the form matters.
416 if self.invites_required() && invite_code.is_none() {
417 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
418 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent419 if !claimable {
API and MCP server, Rust identity service, registration, site redesign420 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent421 "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 redesign422 );
423 }
424 let well_formed_email = email
425 .split_once('@')
426 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
427 && !email.contains(char::is_whitespace);
428 if !well_formed_email {
429 return invalid("Enter a valid email address.");
430 }
431 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning432 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign433 }
434 let taken = self
435 .db
Agents as a team: lifecycle, merge queue, billing and a new shell436 // Usernames and workspaces share one namespace, so that a name
437 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look438 // An address is taken once an account has confirmed it; an
439 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell440 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look441 "SELECT username FROM users WHERE username = ?
442 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 shell443 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
444 )
445 .bind(&[
446 username.as_str().into(),
447 email.as_str().into(),
448 username.as_str().into(),
449 ])?
API and MCP server, Rust identity service, registration, site redesign450 .first::<serde_json::Value>(None)
451 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look452 // A renamed workspace's old slug stays reserved for it a while, and
453 // a deleted workspace's for good.
454 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign455 return Ok(Outcome::fail(
456 FailureCode::Conflict,
457 "That username or email is already registered.",
458 ));
459 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look460 let password_hash = crypto::hash_password(&a.password);
461 let user = match self
462 .create_account(invites::NewAccount {
463 username: &username,
464 email: &email,
465 password_hash: &password_hash,
466 verified: false,
467 invite_code,
468 client: a.client.as_deref(),
469 })
470 .await?
471 {
472 Outcome::Ok(user) => user,
473 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign474 };
Email verification, password reset, and Git for AI scale positioning475 // 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 quotas476 // An invite sent to this address confirmed it already (invites.rs).
477 if !user.verified
478 && let Err(error) = self.send_verification(&user, &email).await
479 {
Email verification, password reset, and Git for AI scale positioning480 worker::console_error!("verification email failed: {error}");
481 }
API and MCP server, Rust identity service, registration, site redesign482 self.start_session(user).await
483 }
484
485 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 look486 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
487 Ok(user) => user,
488 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign489 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign491 self.start_session(user).await
492 }
493
Merge main (membership, two-factor, GitHub repo roles) into tokens494 /// Starts a session for someone who just proved their password (or
495 /// GitHub account). With two-factor authentication on, it starts none:
496 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign497 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens498 if self.two_factor_enabled(&user.id).await? {
499 let challenge = self.issue_challenge(&user.id).await?;
500 return Ok(Outcome::Ok(SignedIn {
501 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
502 session_token: String::new(),
503 two_factor_challenge: Some(challenge),
504 }));
505 }
506 self.session_for(user).await
507 }
508
509 /// A new session for `user`, who has proved who they are in full.
510 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
API and MCP server, Rust identity service, registration, site redesign511 let session_token = crypto::random_hex(32);
512 self.db
RFC 3339 timestamps in identity and repos513 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look514 // Signing in is proof it is the person: see security.rs.
515 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos516 sql_after(SESSION_TTL_SECONDS)
517 ))
API and MCP server, Rust identity service, registration, site redesign518 .bind(&[
519 crypto::sha256_hex(&session_token).into(),
520 user.id.as_str().into(),
521 ])?
522 .run()
523 .await?;
524 Ok(Outcome::Ok(SignedIn {
525 user,
526 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens527 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign528 }))
529 }
530
531 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
532 self.db
533 .prepare("DELETE FROM sessions WHERE id = ?")
534 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
535 .run()
536 .await?;
537 Ok(())
538 }
539
540 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
541 self.find_user(
RFC 3339 timestamps in identity and repos542 &format!(
Workspace names and icons, and a component kit for every control543 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
544 users.avatar
RFC 3339 timestamps in identity and repos545 FROM sessions JOIN users ON users.id = sessions.user_id
546 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
547 ),
API and MCP server, Rust identity service, registration, site redesign548 &crypto::sha256_hex(&a.session_token),
549 )
550 .await
551 }
552
553 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
554 // Like GitHub, a token alone identifies its user.
555 if a.secret.starts_with(TOKEN_PREFIX) {
556 self.user_for_access_token(&a.secret).await
557 } else {
558 self.user_for_password(&a.username, &a.secret).await
559 }
560 }
561
562 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
563 self.find_user(
Email verification, password reset, and Git for AI scale positioning564 "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 redesign565 JOIN users ON users.id = ssh_keys.user_id
566 WHERE fingerprint = ?",
567 &a.fingerprint,
568 )
569 .await
570 }
571
572 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories573 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning574 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign575 &a.username.to_lowercase(),
576 )
577 .await
578 }
579
Inbox: threads, reasons, subscriptions and watching580 /// `notify_by_email`: an inbox item, emailed to the person it is for,
581 /// only at a confirmed address and only while they can still read the
582 /// repository it is about. Returns whether it was sent.
583 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
584 #[derive(Deserialize)]
585 struct Address {
586 email: Option<String>,
587 }
588 let user = self
589 .find_user(
590 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
591 &a.username.to_lowercase(),
592 )
593 .await?;
594 let Some(user) = user.filter(|user| user.verified) else {
595 return Ok(false);
596 };
597 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
598 &self.env.service("REPOS")?,
599 "readable",
600 &g1t_contracts::repos::ReadableArgs {
601 ids: vec![a.repo_id.clone()],
602 viewer: Some(user.clone()),
603 },
604 )
605 .await?;
606 if readable.is_empty() {
607 return Ok(false);
608 }
609 let address = self
610 .db
611 .prepare("SELECT email FROM users WHERE id = ?")
612 .bind(&[user.id.as_str().into()])?
613 .first::<Address>(None)
614 .await?
615 .and_then(|row| row.email)
616 .filter(|email| !email.trim().is_empty());
617 let Some(address) = address else {
618 return Ok(false);
619 };
620 email::send_notification(&self.env, &address, &a).await?;
621 Ok(true)
622 }
623
What happened across an outcome, as a feed beside its graph624 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
625 #[derive(serde::Deserialize)]
626 struct Named {
627 id: String,
628 name: String,
629 }
630 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
631 let mut names = std::collections::HashMap::new();
632 if ids.is_empty() {
633 return Ok(names);
634 }
635 let marks = vec!["?"; ids.len()].join(", ");
636 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
637 for sql in [
638 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
639 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
640 ] {
641 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
642 names.insert(row.id, row.name);
643 }
644 }
645 Ok(names)
646 }
647
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97648 /// `accounts`: the accounts behind these ids (at most 200), each with
649 /// its username and avatar, for lists that keep ids, such as who
650 /// starred a repository. Ids of no account are left out.
651 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
652 #[derive(serde::Deserialize)]
653 struct Row {
654 id: String,
655 username: String,
656 avatar: Option<String>,
657 }
658 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
659 let mut found = std::collections::HashMap::new();
660 if ids.is_empty() {
661 return Ok(found);
662 }
663 let marks = vec!["?"; ids.len()].join(", ");
664 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
665 let rows = self
666 .db
667 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
668 .bind(&bind)?
669 .all()
670 .await?
671 .results::<Row>()?;
672 for row in rows {
673 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
674 }
675 Ok(found)
676 }
677
API and MCP server, Rust identity service, registration, site redesign678 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
679 let rows = self
680 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca681 .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 redesign682 .bind(&[a.user.id.into()])?
683 .all()
684 .await?
685 .results::<KeyRow>()?;
686 Ok(rows.into_iter().map(SshKey::from).collect())
687 }
688
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge689 /// The account (user id) that registered each key, by fingerprint
690 /// (`SHA256:…`). At most 100; unknown keys are left out.
691 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
692 #[derive(serde::Deserialize)]
693 struct Row {
694 fingerprint: String,
695 user_id: String,
696 }
697 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
698 if fingerprints.is_empty() {
699 return Ok(std::collections::HashMap::new());
700 }
701 let marks = vec!["?"; fingerprints.len()].join(", ");
702 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
703 Ok(self
704 .db
705 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
706 .bind(&binds)?
707 .all()
708 .await?
709 .results::<Row>()?
710 .into_iter()
711 .map(|row| (row.fingerprint, row.user_id))
712 .collect())
713 }
714
API and MCP server, Rust identity service, registration, site redesign715 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
716 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
717 return Ok(Outcome::fail(
718 FailureCode::Invalid,
719 "That is not a valid OpenSSH public key.",
720 ));
721 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca722 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
723 if self.key_in_use(&key.fingerprint).await? {
724 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign725 }
726 let now = now_ms();
727 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
728 .into_iter()
729 .find(|candidate| !candidate.is_empty())
730 .unwrap_or_default()
731 .to_owned();
732 let row = KeyRow {
733 id: new_id("key", now),
734 title,
735 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos736 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca737 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign738 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca739 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign740 .prepare(
741 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
742 VALUES (?, ?, ?, ?, ?, ?)",
743 )
744 .bind(&[
745 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca746 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign747 row.title.as_str().into(),
748 key.public_key.into(),
749 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos750 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign751 ])?
752 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca753 .await;
754 // Added at the same moment elsewhere: the trigger or the unique
755 // index refused it.
756 if let Err(error) = inserted {
757 if self.key_in_use(&row.fingerprint).await? {
758 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
759 }
760 return Err(error);
761 }
Merge main (membership, two-factor, GitHub repo roles) into tokens762 let shown = format!("{} ({})", row.title, row.fingerprint);
763 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
764 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign765 Ok(Outcome::Ok(row.into()))
766 }
767
Merge main (membership, two-factor, GitHub repo roles) into tokens768 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca769 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens770 #[derive(Deserialize)]
771 struct Removed {
772 title: String,
773 fingerprint: String,
774 }
775 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca776 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens777 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca778 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens779 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca780 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens781 if let Some(removed) = removed {
782 let shown = format!("{} ({})", removed.title, removed.fingerprint);
783 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
784 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
785 }
API and MCP server, Rust identity service, registration, site redesign786 Ok(())
787 }
788}
789
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas790/// 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 member791/// the last summary's window was still open, so none waits on a later one;
792/// and deleted workspaces past their restore window are purged
793/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas794#[event(scheduled)]
795async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
796 let Ok(db) = env.d1("DB") else { return };
797 let identity = Identity { db, env };
798 if let Err(error) = identity.notify_staff_of_requests().await {
799 worker::console_error!("waitlist summary: {error}");
800 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member801 if let Err(error) = identity.purge_due_workspaces().await {
802 worker::console_error!("workspace purge: {error}");
803 }
Merge main (membership, two-factor, GitHub repo roles) into tokens804 // Once: creators of repositories made before they got Admin (members.rs).
805 if let Err(error) = identity.backfill_creator_grants().await {
806 worker::console_error!("creator grants: {error}");
807 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas808}
809
API and MCP server, Rust identity service, registration, site redesign810#[event(fetch)]
811async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
812 let Some(method) = rpc_method(&request) else {
813 return Response::error("Not found", 404);
814 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents815 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
816 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign817 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents818 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign819
Fast pages, required checks on the branch, self-hosted runners, honest incidents820 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette821 "register" => {
822 let outcome = identity.register(args(body)?).await?;
823 if let Outcome::Ok(signed_in) = &outcome {
824 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
825 }
826 reply(&outcome)
827 }
API and MCP server, Rust identity service, registration, site redesign828 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette829 "create_workspace" => {
830 let outcome = identity.create_workspace(args(body)?).await?;
831 if let Outcome::Ok(workspace) = &outcome {
832 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
833 }
834 reply(&outcome)
835 }
Workspaces own repositories836 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
837 "list_members" => reply(&identity.list_members(args(body)?).await?),
838 "add_member" => reply(&identity.add_member(args(body)?).await?),
839 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens840 // Owners, roles, leaving and member privileges; see members.rs.
841 "update_member" => reply(&identity.update_member(args(body)?).await?),
842 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
843 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
844 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
845 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
846 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette847 "update_workspace" => {
848 let outcome = identity.update_workspace(args(body)?).await?;
849 if let Outcome::Ok(workspace) = &outcome {
850 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
851 }
852 reply(&outcome)
853 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains854 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
855 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
856 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'857 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look858 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
859 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
860 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette861 "set_workspace_avatar" => {
862 let outcome = identity.set_workspace_avatar(args(body)?).await?;
863 if let Outcome::Ok(workspace) = &outcome {
864 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
865 }
866 reply(&outcome)
867 }
868 "set_user_avatar" => {
869 let a: SetUserAvatarArgs = args(body)?;
870 let (username, id) = (a.user.username.clone(), a.user.id.clone());
871 let outcome = identity.set_user_avatar(a).await?;
872 if matches!(outcome, Outcome::Ok(_)) {
873 identity.announce_user(&username, Some(&id)).await;
874 }
875 reply(&outcome)
876 }
Agents as a team: lifecycle, merge queue, billing and a new shell877 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
878 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
879 "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 look880 // Signing in with GitHub; see github.rs.
881 "github_enabled" => reply(&identity.github_enabled()),
882 "github_start" => reply(&identity.github_start(args(body)?).await?),
883 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
884 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
885 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
886 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
887 "github_account" => reply(&identity.github_account(args(body)?).await?),
888 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
889 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
890 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
891 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications892 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
893 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
894 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
895 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
896 "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 step897 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API898 "device_start" => reply(&identity.device_start(args(body)?).await?),
899 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
900 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
901 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning902 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
903 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
904 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
905 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look906 // A person's email addresses; see emails.rs and security.rs.
907 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
908 "add_email" => reply(&identity.add_email(args(body)?).await?),
909 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
910 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
911 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
912 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens913 // Two-factor authentication; see two_factor.rs.
914 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
915 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
916 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
917 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
918 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
919 "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 look920 "security_log" => reply(&identity.security_log(args(body)?).await?),
921 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
922 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
923 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
924 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
925 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign926 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
927 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
928 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
929 "user_for_access_token" => {
930 let a: TokenArgs = args(body)?;
931 reply(&identity.user_for_access_token(&a.token).await?)
932 }
933 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
934 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph935 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97936 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching937 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains938 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette939 "update_profile" => {
940 let outcome = identity.update_profile(args(body)?).await?;
941 if let Outcome::Ok(profile) = &outcome {
942 identity.announce_user(&profile.username, None).await;
943 }
944 reply(&outcome)
945 }
946 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains947 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign948 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge949 // Services only: who registered each key, for verifying commit
950 // signatures (repos' signatures.rs).
951 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign952 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca953 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
954 // A repository's deploy keys, and who an SSH key signs in as; see
955 // deploy_keys.rs.
956 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
957 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
958 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
959 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
960 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign961 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
962 "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 step963 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity964 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
965 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
966 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
967 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
968 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
969 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
970 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
971 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell972 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
973 "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 API974 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
975 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
976 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'977 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
978 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens979 "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 look980 // Invites and the waitlist; see invites.rs.
981 "registration" => reply(&identity.registration_mode()),
982 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
983 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
984 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
985 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
986 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
987 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
988 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
989 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
990 "request_access" => reply(&identity.request_access(args(body)?).await?),
991 // Who has access to a repository; see access.rs.
992 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
993 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
994 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
995 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
996 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
997 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
998 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
999 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
1000 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'1001 // Where a workspace keeps its repositories' git data (EU residency).
1002 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
1003 "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 look1004 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1005 "forget_repo_access" => {
1006 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
1007 // A purged repository's deploy keys go with its access.
1008 identity.forget_deploy_keys(&a.repo_id).await?;
1009 reply(&identity.forget_repo_access(a).await?)
1010 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1011 // Teams (teams.rs).
1012 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
1013 "get_team" => reply(&identity.get_team(args(body)?).await?),
1014 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1015 "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 bar1016 "update_team" => reply(&identity.update_team(args(body)?).await?),
1017 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
1018 "team_members" => reply(&identity.team_members(args(body)?).await?),
1019 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
1020 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
1021 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
1022 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
1023 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
1024 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
1025 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1026 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1027 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1028 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1029 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1030 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1031 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1032 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1033 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1034 "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 quotas1035 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1036 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1037 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1038 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1039 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1040 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1041 "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 member1042 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1043 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1044 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1045 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1046 // Workspace aliases, set by staff only; see aliases.rs.
1047 "admin_aliases" => reply(&identity.admin_aliases().await?),
1048 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1049 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1050 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1051 };
1052 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1053}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1054
1055#[cfg(test)]
1056mod register_tests {
1057 use super::*;
1058
1059 #[test]
1060 fn nobody_registers_as_g1t() {
1061 // What register checks the username with, whatever its case.
1062 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1063 assert_eq!(claimable_namespace(username), None, "{username}");
1064 }
1065 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1066 }
1067}

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