Skip to content
1,053 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;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)7mod account_deletion;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace8mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'9mod aliases;
Workspace names and icons, and a component kit for every control10mod avatars;
API and MCP server, Rust identity service, registration, site redesign11mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12mod deletion;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca13mod deploy_keys;
Device sign-in replaces registering and minting tokens over the API14mod device;
Search across all of g1t, Explore, and a command palette15mod directory;
Email verification, password reset, and Git for AI scale positioning16mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17mod emails;
18mod github;
19mod invites;
Merge main (membership, two-factor, GitHub repo roles) into tokens20mod members;
OAuth 2.1 sign-in for MCP clients and other applications21mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person22mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains23mod profiles;
24mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'25mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API26mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27mod security;
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)28mod shared_invites;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar29mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity31mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell32mod tokens;
Merge main (membership, two-factor, GitHub repo roles) into tokens33mod two_factor;
Workspaces own repositories34mod workspaces;
API and MCP server, Rust identity service, registration, site redesign35
36use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos37use 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-agent38use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign39use g1t_kit::{args, now_ms, reply, rpc_method};
40use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell41use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign42use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas43use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign44
RFC 3339 timestamps in identity and repos45const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
46const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign47const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning48const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
49
50/// A user as selected from the database; `verified` arrives as 0 or 1.
51#[derive(Deserialize)]
52struct Account {
53 id: String,
54 username: String,
55 verified: u8,
Workspace names and icons, and a component kit for every control56 /// Selected only where the person is being shown to themselves.
57 #[serde(default)]
58 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning59}
60
61impl From<Account> for User {
62 fn from(row: Account) -> Self {
63 User {
64 id: row.id,
65 username: row.username,
66 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control67 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell68 ..User::default()
Email verification, password reset, and Git for AI scale positioning69 }
70 }
71}
API and MCP server, Rust identity service, registration, site redesign72
73#[derive(Deserialize)]
74struct UserRow {
75 id: String,
76 username: String,
77 password_hash: String,
Email verification, password reset, and Git for AI scale positioning78 verified: u8,
API and MCP server, Rust identity service, registration, site redesign79}
80
Email verification, password reset, and Git for AI scale positioning81/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign82#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning83struct TokenOwner {
84 id: String,
85 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 /// The address a link was sent to; null on links from before accounts
87 /// had several, which are for the primary.
88 #[serde(default)]
89 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning90}
91
92#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign93struct KeyRow {
94 id: String,
95 title: String,
96 fingerprint: String,
RFC 3339 timestamps in identity and repos97 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca98 #[serde(default)]
99 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign100}
101
102impl From<KeyRow> for SshKey {
103 fn from(row: KeyRow) -> Self {
104 SshKey {
105 id: row.id,
106 title: row.title,
107 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos108 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca109 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign110 }
111 }
112}
113
114struct Identity {
115 db: D1Database,
Email verification, password reset, and Git for AI scale positioning116 env: Env,
API and MCP server, Rust identity service, registration, site redesign117}
118
119impl Identity {
Workspaces own repositories120 /// Runs a query that returns at most one user, for showing to others:
121 /// without their workspaces.
122 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning123 Ok(self
124 .db
API and MCP server, Rust identity service, registration, site redesign125 .prepare(sql)
126 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning127 .first::<Account>(None)
128 .await?
129 .map(User::from))
130 }
131
Workspaces own repositories132 /// Attaches the workspaces a user belongs to, so that any service can
133 /// authorize them without asking again.
134 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
135 let Some(mut user) = user else {
136 return Ok(None);
137 };
Merge main (membership, two-factor, GitHub repo roles) into tokens138 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look139 // Roles on single repositories, under the same policy (access.rs).
140 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens141 // Access to a workspace is used only within its policy; see security.rs.
142 let within = self.within_policy(&user.id, memberships, grants).await?;
143 user.workspaces = within.memberships;
144 user.grants = within.grants;
145 user.held = within.held;
Workspaces own repositories146 Ok(Some(user))
147 }
148
149 /// Runs a query that resolves credentials to at most one user.
150 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
151 let user = self.find_public_user(sql, param).await?;
152 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign153 }
154
Email verification, password reset, and Git for AI scale positioning155 /// Consumes a token of `kind`, returning its owner if it was valid.
156 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
157 let id = crypto::sha256_hex(token);
158 let owner = self
159 .db
RFC 3339 timestamps in identity and repos160 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning162 JOIN users ON users.id = email_tokens.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)163 WHERE email_tokens.id = ? AND email_tokens.kind = ? AND users.deleted_at IS NULL
RFC 3339 timestamps in identity and repos164 AND email_tokens.expires_at > {SQL_NOW}"
165 ))
Email verification, password reset, and Git for AI scale positioning166 .bind(&[id.as_str().into(), kind.into()])?
167 .first::<TokenOwner>(None)
168 .await?;
169 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look170 // Every outstanding token of this kind dies with the one used:
171 // every reset link, and every confirmation link for the same
172 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning173 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look174 .prepare(
175 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
176 AND (?2 = 'reset' OR email_id IS ?3)",
177 )
178 .bind(&[
179 owner.id.as_str().into(),
180 kind.into(),
181 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
182 ])?
Email verification, password reset, and Git for AI scale positioning183 .run()
184 .await?;
185 }
186 Ok(owner)
187 }
188
189 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 look190 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
191 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
192 }
193 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning194 }
195
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)196 /// The link in a confirmation email, followed: signed in or not. It
197 /// ends the code sent with it (emails.rs).
198 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<g1t_contracts::accounts::EmailConfirmed>> {
Email verification, password reset, and Git for AI scale positioning199 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
200 return Ok(Outcome::fail(
201 FailureCode::Invalid,
202 "This confirmation link is not valid or has expired.",
203 ));
204 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)205 self.confirm_address(&owner.id, owner.email_id.as_deref()).await
Email verification, password reset, and Git for AI scale positioning206 }
207
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 /// Any confirmed address of an account can ask for a reset; so can the
209 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning210 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 look211 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
212 && match a.client.as_deref() {
213 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
214 None => true,
215 };
216 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 exists217 // A failure from here on happens only for a real account, so it
218 // is logged, never answered: the reply below stays the same.
219 if let Err(error) = self.send_reset(&target).await {
220 worker::console_error!("password reset for a known address failed: {error}");
221 }
222 }
223 // The same answer either way, so addresses cannot be probed.
224 Ok(true)
225 }
226
227 /// Saves a reset link for `target` and mails it, telling the account's
228 /// other addresses.
229 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
230 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look231 let token = crypto::random_hex(32);
232 self.db
233 .prepare(format!(
234 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
235 VALUES (?, ?, 'reset', {}, ?)",
236 sql_after(RESET_TTL_SECONDS)
237 ))
238 .bind(&[
239 crypto::sha256_hex(&token).into(),
240 target.user_id.as_str().into(),
241 target.email_id.as_str().into(),
242 ])?
243 .run()
Email verification, password reset, and Git for AI scale positioning244 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look245 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
246 // The primary and the backup hear of it when it went elsewhere.
247 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
248 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
249 let change = format!("A password reset was asked for through {}", target.display);
250 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
251 worker::console_error!("security notice failed: {error}");
252 }
253 }
Email verification, password reset, and Git for AI scale positioning254 }
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 exists255 Ok(())
Email verification, password reset, and Git for AI scale positioning256 }
257
258 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
259 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
260 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
261 }
262 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
263 return Ok(Outcome::fail(
264 FailureCode::Invalid,
265 "This reset link is not valid or has expired.",
266 ));
267 };
268 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look269 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning270 .bind(&[
271 crypto::hash_password(&a.password).into(),
272 owner.id.as_str().into(),
273 ])?
274 .run()
275 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look276 // Following an emailed link also proves the address it went to
277 // (unless another account confirmed it first).
278 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
279 // Anyone signed in with the old password is signed out, and nobody
280 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning281 self.db
282 .prepare("DELETE FROM sessions WHERE user_id = ?")
283 .bind(&[owner.id.as_str().into()])?
284 .run()
285 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look286 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
287 self.log_security(&owner.id, "password_changed", None, None).await;
288 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
289 let verified = self
290 .find_public_user(
291 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
292 &owner.id,
293 )
294 .await?
295 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning296 Ok(Outcome::Ok(User {
297 id: owner.id,
298 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look299 verified,
Workspaces own repositories300 ..User::default()
Email verification, password reset, and Git for AI scale positioning301 }))
302 }
303
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 /// The account a login names: a username, or any confirmed address.
305 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
306 let login = login.trim().to_lowercase();
307 let (column, value) = if login.contains('@') {
308 match self.user_with_verified_email(&login).await? {
309 Some(id) => ("id", id),
310 None => return Ok(None),
311 }
312 } else {
313 ("username", login)
314 };
315 self.db
316 .prepare(format!(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)317 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users
318 WHERE {column} = ? AND deleted_at IS NULL"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 ))
320 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign321 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 .await
323 }
324
325 /// Checks a password for a login, throttled (see throttle.rs). The
326 /// refusal is one of two messages, the same for every account.
327 async fn checked_password(
328 &self,
329 login: &str,
330 password: &str,
331 client: Option<&str>,
332 ) -> Result<std::result::Result<User, &'static str>> {
333 let row = self.password_row(login).await?;
334 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
335 let (account_key, client_key) = Identity::password_keys(&subject, client);
336 if self.password_locked(&account_key, client_key.as_deref()).await? {
337 return Ok(Err(throttle::THROTTLED));
338 }
339 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
340 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
341 Some(row) => {
342 self.clear(&account_key).await?;
343 Ok(Ok(User {
344 id: row.id,
345 username: row.username,
346 verified: row.verified != 0,
347 ..User::default()
348 }))
349 }
350 None => {
351 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
352 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
353 Ok(Err("Incorrect username or password."))
354 }
355 }
356 }
357
Merge main (membership, two-factor, GitHub repo roles) into tokens358 /// Git over HTTPS with the account's password. With two-factor
359 /// authentication on, a password alone is never enough: use an access
360 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look361 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
362 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens363 if let Some(user) = &user
364 && self.two_factor_enabled(&user.id).await?
365 {
366 return Ok(None);
367 }
Workspaces own repositories368 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign369 }
370
371 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
372 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-agent373 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign374 let email = a.email.trim().to_lowercase();
375 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 look376 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
377 // The invite first: without one, nothing else on the form matters.
378 if self.invites_required() && invite_code.is_none() {
379 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
380 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent381 if !claimable {
API and MCP server, Rust identity service, registration, site redesign382 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent383 "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 redesign384 );
385 }
386 let well_formed_email = email
387 .split_once('@')
388 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
389 && !email.contains(char::is_whitespace);
390 if !well_formed_email {
391 return invalid("Enter a valid email address.");
392 }
393 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning394 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign395 }
396 let taken = self
397 .db
Agents as a team: lifecycle, merge queue, billing and a new shell398 // Usernames and workspaces share one namespace, so that a name
399 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look400 // An address is taken once an account has confirmed it; an
401 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell402 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look403 "SELECT username FROM users WHERE username = ?
404 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 shell405 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
406 )
407 .bind(&[
408 username.as_str().into(),
409 email.as_str().into(),
410 username.as_str().into(),
411 ])?
API and MCP server, Rust identity service, registration, site redesign412 .first::<serde_json::Value>(None)
413 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look414 // A renamed workspace's old slug stays reserved for it a while, and
415 // a deleted workspace's for good.
416 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign417 return Ok(Outcome::fail(
418 FailureCode::Conflict,
419 "That username or email is already registered.",
420 ));
421 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look422 let password_hash = crypto::hash_password(&a.password);
423 let user = match self
424 .create_account(invites::NewAccount {
425 username: &username,
426 email: &email,
427 password_hash: &password_hash,
428 verified: false,
429 invite_code,
430 client: a.client.as_deref(),
431 })
432 .await?
433 {
434 Outcome::Ok(user) => user,
435 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign436 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)437 // The account exists either way; the email can be sent again from
438 // the confirmation page. It carries a code and a link (emails.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas439 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)440 && let Err(error) = self.send_primary_confirmation(&user.id, &user.username).await
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas441 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)442 worker::console_error!("confirmation email failed: {error}");
Email verification, password reset, and Git for AI scale positioning443 }
API and MCP server, Rust identity service, registration, site redesign444 self.start_session(user).await
445 }
446
447 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 look448 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
449 Ok(user) => user,
450 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign451 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look452 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign453 self.start_session(user).await
454 }
455
Merge main (membership, two-factor, GitHub repo roles) into tokens456 /// Starts a session for someone who just proved their password (or
457 /// GitHub account). With two-factor authentication on, it starts none:
458 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign459 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens460 if self.two_factor_enabled(&user.id).await? {
461 let challenge = self.issue_challenge(&user.id).await?;
462 return Ok(Outcome::Ok(SignedIn {
463 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
464 session_token: String::new(),
465 two_factor_challenge: Some(challenge),
466 }));
467 }
468 self.session_for(user).await
469 }
470
471 /// A new session for `user`, who has proved who they are in full.
472 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)473 // Whichever way it was proved, a deleted account starts none
474 // (account_deletion.rs).
475 if !self.account_live(&user.id).await? {
476 return Ok(Outcome::fail(FailureCode::Unauthenticated, "Incorrect username or password."));
477 }
API and MCP server, Rust identity service, registration, site redesign478 let session_token = crypto::random_hex(32);
479 self.db
RFC 3339 timestamps in identity and repos480 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look481 // Signing in is proof it is the person: see security.rs.
482 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos483 sql_after(SESSION_TTL_SECONDS)
484 ))
API and MCP server, Rust identity service, registration, site redesign485 .bind(&[
486 crypto::sha256_hex(&session_token).into(),
487 user.id.as_str().into(),
488 ])?
489 .run()
490 .await?;
491 Ok(Outcome::Ok(SignedIn {
492 user,
493 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens494 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign495 }))
496 }
497
498 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
499 self.db
500 .prepare("DELETE FROM sessions WHERE id = ?")
501 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
502 .run()
503 .await?;
504 Ok(())
505 }
506
507 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
508 self.find_user(
RFC 3339 timestamps in identity and repos509 &format!(
Workspace names and icons, and a component kit for every control510 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
511 users.avatar
RFC 3339 timestamps in identity and repos512 FROM sessions JOIN users ON users.id = sessions.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)513 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW} AND users.deleted_at IS NULL"
RFC 3339 timestamps in identity and repos514 ),
API and MCP server, Rust identity service, registration, site redesign515 &crypto::sha256_hex(&a.session_token),
516 )
517 .await
518 }
519
520 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
521 // Like GitHub, a token alone identifies its user.
522 if a.secret.starts_with(TOKEN_PREFIX) {
523 self.user_for_access_token(&a.secret).await
524 } else {
525 self.user_for_password(&a.username, &a.secret).await
526 }
527 }
528
529 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
530 self.find_user(
Email verification, password reset, and Git for AI scale positioning531 "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 redesign532 JOIN users ON users.id = ssh_keys.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)533 WHERE fingerprint = ? AND users.deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign534 &a.fingerprint,
535 )
536 .await
537 }
538
539 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories540 self.find_public_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)541 // A deleted account is nobody's to find, mention or add.
542 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign543 &a.username.to_lowercase(),
544 )
545 .await
546 }
547
Inbox: threads, reasons, subscriptions and watching548 /// `notify_by_email`: an inbox item, emailed to the person it is for,
549 /// only at a confirmed address and only while they can still read the
550 /// repository it is about. Returns whether it was sent.
551 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
552 #[derive(Deserialize)]
553 struct Address {
554 email: Option<String>,
555 }
556 let user = self
557 .find_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)558 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
Inbox: threads, reasons, subscriptions and watching559 &a.username.to_lowercase(),
560 )
561 .await?;
562 let Some(user) = user.filter(|user| user.verified) else {
563 return Ok(false);
564 };
565 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
566 &self.env.service("REPOS")?,
567 "readable",
568 &g1t_contracts::repos::ReadableArgs {
569 ids: vec![a.repo_id.clone()],
570 viewer: Some(user.clone()),
571 },
572 )
573 .await?;
574 if readable.is_empty() {
575 return Ok(false);
576 }
577 let address = self
578 .db
579 .prepare("SELECT email FROM users WHERE id = ?")
580 .bind(&[user.id.as_str().into()])?
581 .first::<Address>(None)
582 .await?
583 .and_then(|row| row.email)
584 .filter(|email| !email.trim().is_empty());
585 let Some(address) = address else {
586 return Ok(false);
587 };
588 email::send_notification(&self.env, &address, &a).await?;
589 Ok(true)
590 }
591
What happened across an outcome, as a feed beside its graph592 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
593 #[derive(serde::Deserialize)]
594 struct Named {
595 id: String,
596 name: String,
597 }
598 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
599 let mut names = std::collections::HashMap::new();
600 if ids.is_empty() {
601 return Ok(names);
602 }
603 let marks = vec!["?"; ids.len()].join(", ");
604 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
605 for sql in [
606 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
607 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
608 ] {
609 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
610 names.insert(row.id, row.name);
611 }
612 }
613 Ok(names)
614 }
615
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97616 /// `accounts`: the accounts behind these ids (at most 200), each with
617 /// its username and avatar, for lists that keep ids, such as who
618 /// starred a repository. Ids of no account are left out.
619 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
620 #[derive(serde::Deserialize)]
621 struct Row {
622 id: String,
623 username: String,
624 avatar: Option<String>,
625 }
626 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
627 let mut found = std::collections::HashMap::new();
628 if ids.is_empty() {
629 return Ok(found);
630 }
631 let marks = vec!["?"; ids.len()].join(", ");
632 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
633 let rows = self
634 .db
635 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
636 .bind(&bind)?
637 .all()
638 .await?
639 .results::<Row>()?;
640 for row in rows {
641 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
642 }
643 Ok(found)
644 }
645
API and MCP server, Rust identity service, registration, site redesign646 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
647 let rows = self
648 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca649 .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 redesign650 .bind(&[a.user.id.into()])?
651 .all()
652 .await?
653 .results::<KeyRow>()?;
654 Ok(rows.into_iter().map(SshKey::from).collect())
655 }
656
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge657 /// The account (user id) that registered each key, by fingerprint
658 /// (`SHA256:…`). At most 100; unknown keys are left out.
659 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
660 #[derive(serde::Deserialize)]
661 struct Row {
662 fingerprint: String,
663 user_id: String,
664 }
665 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
666 if fingerprints.is_empty() {
667 return Ok(std::collections::HashMap::new());
668 }
669 let marks = vec!["?"; fingerprints.len()].join(", ");
670 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
671 Ok(self
672 .db
673 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
674 .bind(&binds)?
675 .all()
676 .await?
677 .results::<Row>()?
678 .into_iter()
679 .map(|row| (row.fingerprint, row.user_id))
680 .collect())
681 }
682
API and MCP server, Rust identity service, registration, site redesign683 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
684 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
685 return Ok(Outcome::fail(
686 FailureCode::Invalid,
687 "That is not a valid OpenSSH public key.",
688 ));
689 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca690 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
691 if self.key_in_use(&key.fingerprint).await? {
692 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign693 }
694 let now = now_ms();
695 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
696 .into_iter()
697 .find(|candidate| !candidate.is_empty())
698 .unwrap_or_default()
699 .to_owned();
700 let row = KeyRow {
701 id: new_id("key", now),
702 title,
703 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos704 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca705 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign706 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca707 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign708 .prepare(
709 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
710 VALUES (?, ?, ?, ?, ?, ?)",
711 )
712 .bind(&[
713 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca714 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign715 row.title.as_str().into(),
716 key.public_key.into(),
717 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos718 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign719 ])?
720 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca721 .await;
722 // Added at the same moment elsewhere: the trigger or the unique
723 // index refused it.
724 if let Err(error) = inserted {
725 if self.key_in_use(&row.fingerprint).await? {
726 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
727 }
728 return Err(error);
729 }
Merge main (membership, two-factor, GitHub repo roles) into tokens730 let shown = format!("{} ({})", row.title, row.fingerprint);
731 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
732 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign733 Ok(Outcome::Ok(row.into()))
734 }
735
Merge main (membership, two-factor, GitHub repo roles) into tokens736 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca737 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens738 #[derive(Deserialize)]
739 struct Removed {
740 title: String,
741 fingerprint: String,
742 }
743 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca744 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens745 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca746 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens747 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca748 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens749 if let Some(removed) = removed {
750 let shown = format!("{} ({})", removed.title, removed.fingerprint);
751 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
752 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
753 }
API and MCP server, Rust identity service, registration, site redesign754 Ok(())
755 }
756}
757
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas758/// 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 member759/// the last summary's window was still open, so none waits on a later one;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)760/// and deleted workspaces and accounts past their restore window are purged
761/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas762#[event(scheduled)]
763async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
764 let Ok(db) = env.d1("DB") else { return };
765 let identity = Identity { db, env };
766 if let Err(error) = identity.notify_staff_of_requests().await {
767 worker::console_error!("waitlist summary: {error}");
768 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member769 if let Err(error) = identity.purge_due_workspaces().await {
770 worker::console_error!("workspace purge: {error}");
771 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)772 // And deleted accounts past theirs (account_deletion.rs).
773 if let Err(error) = identity.purge_due_accounts().await {
774 worker::console_error!("account purge: {error}");
775 }
Merge main (membership, two-factor, GitHub repo roles) into tokens776 // Once: creators of repositories made before they got Admin (members.rs).
777 if let Err(error) = identity.backfill_creator_grants().await {
778 worker::console_error!("creator grants: {error}");
779 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas780}
781
API and MCP server, Rust identity service, registration, site redesign782#[event(fetch)]
783async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
784 let Some(method) = rpc_method(&request) else {
785 return Response::error("Not found", 404);
786 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents787 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
788 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign789 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents790 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign791
Fast pages, required checks on the branch, self-hosted runners, honest incidents792 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette793 "register" => {
794 let outcome = identity.register(args(body)?).await?;
795 if let Outcome::Ok(signed_in) = &outcome {
796 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
797 }
798 reply(&outcome)
799 }
API and MCP server, Rust identity service, registration, site redesign800 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette801 "create_workspace" => {
802 let outcome = identity.create_workspace(args(body)?).await?;
803 if let Outcome::Ok(workspace) = &outcome {
804 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
805 }
806 reply(&outcome)
807 }
Workspaces own repositories808 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
809 "list_members" => reply(&identity.list_members(args(body)?).await?),
810 "add_member" => reply(&identity.add_member(args(body)?).await?),
811 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens812 // Owners, roles, leaving and member privileges; see members.rs.
813 "update_member" => reply(&identity.update_member(args(body)?).await?),
814 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
815 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
816 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
817 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
818 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette819 "update_workspace" => {
820 let outcome = identity.update_workspace(args(body)?).await?;
821 if let Outcome::Ok(workspace) = &outcome {
822 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
823 }
824 reply(&outcome)
825 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains826 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
827 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
828 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'829 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look830 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
831 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
832 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette833 "set_workspace_avatar" => {
834 let outcome = identity.set_workspace_avatar(args(body)?).await?;
835 if let Outcome::Ok(workspace) = &outcome {
836 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
837 }
838 reply(&outcome)
839 }
840 "set_user_avatar" => {
841 let a: SetUserAvatarArgs = args(body)?;
842 let (username, id) = (a.user.username.clone(), a.user.id.clone());
843 let outcome = identity.set_user_avatar(a).await?;
844 if matches!(outcome, Outcome::Ok(_)) {
845 identity.announce_user(&username, Some(&id)).await;
846 }
847 reply(&outcome)
848 }
Agents as a team: lifecycle, merge queue, billing and a new shell849 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
850 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
851 "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 look852 // Signing in with GitHub; see github.rs.
853 "github_enabled" => reply(&identity.github_enabled()),
854 "github_start" => reply(&identity.github_start(args(body)?).await?),
855 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
856 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
857 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
858 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
859 "github_account" => reply(&identity.github_account(args(body)?).await?),
860 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
861 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
862 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
863 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications864 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
865 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
866 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
867 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
868 "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 step869 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API870 "device_start" => reply(&identity.device_start(args(body)?).await?),
871 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
872 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
873 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning874 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
875 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)876 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
877 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning878 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
879 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look880 // A person's email addresses; see emails.rs and security.rs.
881 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
882 "add_email" => reply(&identity.add_email(args(body)?).await?),
883 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
884 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
885 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
886 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens887 // Two-factor authentication; see two_factor.rs.
888 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
889 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
890 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
891 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
892 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
893 "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 look894 "security_log" => reply(&identity.security_log(args(body)?).await?),
895 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
896 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
897 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
898 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
899 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign900 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
901 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
902 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
903 "user_for_access_token" => {
904 let a: TokenArgs = args(body)?;
905 reply(&identity.user_for_access_token(&a.token).await?)
906 }
907 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
908 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph909 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97910 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching911 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains912 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette913 "update_profile" => {
914 let outcome = identity.update_profile(args(body)?).await?;
915 if let Outcome::Ok(profile) = &outcome {
916 identity.announce_user(&profile.username, None).await;
917 }
918 reply(&outcome)
919 }
920 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains921 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign922 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge923 // Services only: who registered each key, for verifying commit
924 // signatures (repos' signatures.rs).
925 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign926 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca927 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
928 // A repository's deploy keys, and who an SSH key signs in as; see
929 // deploy_keys.rs.
930 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
931 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
932 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
933 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
934 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign935 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
936 "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 step937 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity938 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
939 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
940 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
941 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
942 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
943 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
944 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
945 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell946 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
947 "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 API948 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
949 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
950 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'951 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
952 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens953 "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 look954 // Invites and the waitlist; see invites.rs.
955 "registration" => reply(&identity.registration_mode()),
956 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
957 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
958 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
959 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
960 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
961 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
962 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
963 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
964 "request_access" => reply(&identity.request_access(args(body)?).await?),
965 // Who has access to a repository; see access.rs.
966 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
967 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
968 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
969 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
970 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
971 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
972 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
973 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
974 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'975 // Where a workspace keeps its repositories' git data (EU residency).
976 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
977 "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 look978 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca979 "forget_repo_access" => {
980 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
981 // A purged repository's deploy keys go with its access.
982 identity.forget_deploy_keys(&a.repo_id).await?;
983 reply(&identity.forget_repo_access(a).await?)
984 }
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?),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1016 // Shared invite links for a group; see shared_invites.rs.
1017 "admin_shared_invites" => reply(&identity.admin_shared_invites().await?),
1018 "admin_create_shared_invite" => reply(&identity.admin_create_shared_invite(args(body)?).await?),
1019 "admin_revoke_shared_invite" => reply(&identity.admin_revoke_shared_invite(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1020 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1021 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1022 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1023 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)1024 // Deleting accounts (account_deletion.rs): the person from the
1025 // site, staff from sudo. There is no API route for it.
1026 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1027 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1028 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1029 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1030 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1031 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1032 // Workspace aliases, set by staff only; see aliases.rs.
1033 "admin_aliases" => reply(&identity.admin_aliases().await?),
1034 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1035 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1036 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1037 };
1038 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1039}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1040
1041#[cfg(test)]
1042mod register_tests {
1043 use super::*;
1044
1045 #[test]
1046 fn nobody_registers_as_g1t() {
1047 // What register checks the username with, whatever its case.
1048 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1049 assert_eq!(claimable_namespace(username), None, "{username}");
1050 }
1051 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1052 }
1053}

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