Skip to content
1,058 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,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm430 email_proof: a.email_proof.as_deref(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look431 client: a.client.as_deref(),
432 })
433 .await?
434 {
435 Outcome::Ok(user) => user,
436 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign437 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)438 // The account exists either way; the email can be sent again from
439 // 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 quotas440 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)441 && 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 quotas442 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)443 worker::console_error!("confirmation email failed: {error}");
Email verification, password reset, and Git for AI scale positioning444 }
API and MCP server, Rust identity service, registration, site redesign445 self.start_session(user).await
446 }
447
448 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 look449 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
450 Ok(user) => user,
451 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign452 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look453 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign454 self.start_session(user).await
455 }
456
Merge main (membership, two-factor, GitHub repo roles) into tokens457 /// Starts a session for someone who just proved their password (or
458 /// GitHub account). With two-factor authentication on, it starts none:
459 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign460 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens461 if self.two_factor_enabled(&user.id).await? {
462 let challenge = self.issue_challenge(&user.id).await?;
463 return Ok(Outcome::Ok(SignedIn {
464 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
465 session_token: String::new(),
466 two_factor_challenge: Some(challenge),
467 }));
468 }
469 self.session_for(user).await
470 }
471
472 /// A new session for `user`, who has proved who they are in full.
473 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)474 // Whichever way it was proved, a deleted account starts none
475 // (account_deletion.rs).
476 if !self.account_live(&user.id).await? {
477 return Ok(Outcome::fail(FailureCode::Unauthenticated, "Incorrect username or password."));
478 }
API and MCP server, Rust identity service, registration, site redesign479 let session_token = crypto::random_hex(32);
480 self.db
RFC 3339 timestamps in identity and repos481 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look482 // Signing in is proof it is the person: see security.rs.
483 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos484 sql_after(SESSION_TTL_SECONDS)
485 ))
API and MCP server, Rust identity service, registration, site redesign486 .bind(&[
487 crypto::sha256_hex(&session_token).into(),
488 user.id.as_str().into(),
489 ])?
490 .run()
491 .await?;
492 Ok(Outcome::Ok(SignedIn {
493 user,
494 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens495 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign496 }))
497 }
498
499 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
500 self.db
501 .prepare("DELETE FROM sessions WHERE id = ?")
502 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
503 .run()
504 .await?;
505 Ok(())
506 }
507
508 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
509 self.find_user(
RFC 3339 timestamps in identity and repos510 &format!(
Workspace names and icons, and a component kit for every control511 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
512 users.avatar
RFC 3339 timestamps in identity and repos513 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)514 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW} AND users.deleted_at IS NULL"
RFC 3339 timestamps in identity and repos515 ),
API and MCP server, Rust identity service, registration, site redesign516 &crypto::sha256_hex(&a.session_token),
517 )
518 .await
519 }
520
521 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
522 // Like GitHub, a token alone identifies its user.
523 if a.secret.starts_with(TOKEN_PREFIX) {
524 self.user_for_access_token(&a.secret).await
525 } else {
526 self.user_for_password(&a.username, &a.secret).await
527 }
528 }
529
530 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
531 self.find_user(
Email verification, password reset, and Git for AI scale positioning532 "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 redesign533 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)534 WHERE fingerprint = ? AND users.deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign535 &a.fingerprint,
536 )
537 .await
538 }
539
540 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories541 self.find_public_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)542 // A deleted account is nobody's to find, mention or add.
543 "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 redesign544 &a.username.to_lowercase(),
545 )
546 .await
547 }
548
Inbox: threads, reasons, subscriptions and watching549 /// `notify_by_email`: an inbox item, emailed to the person it is for,
550 /// only at a confirmed address and only while they can still read the
551 /// repository it is about. Returns whether it was sent.
552 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
553 #[derive(Deserialize)]
554 struct Address {
555 email: Option<String>,
556 }
557 let user = self
558 .find_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)559 "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 watching560 &a.username.to_lowercase(),
561 )
562 .await?;
563 let Some(user) = user.filter(|user| user.verified) else {
564 return Ok(false);
565 };
566 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
567 &self.env.service("REPOS")?,
568 "readable",
569 &g1t_contracts::repos::ReadableArgs {
570 ids: vec![a.repo_id.clone()],
571 viewer: Some(user.clone()),
572 },
573 )
574 .await?;
575 if readable.is_empty() {
576 return Ok(false);
577 }
578 let address = self
579 .db
580 .prepare("SELECT email FROM users WHERE id = ?")
581 .bind(&[user.id.as_str().into()])?
582 .first::<Address>(None)
583 .await?
584 .and_then(|row| row.email)
585 .filter(|email| !email.trim().is_empty());
586 let Some(address) = address else {
587 return Ok(false);
588 };
589 email::send_notification(&self.env, &address, &a).await?;
590 Ok(true)
591 }
592
What happened across an outcome, as a feed beside its graph593 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
594 #[derive(serde::Deserialize)]
595 struct Named {
596 id: String,
597 name: String,
598 }
599 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
600 let mut names = std::collections::HashMap::new();
601 if ids.is_empty() {
602 return Ok(names);
603 }
604 let marks = vec!["?"; ids.len()].join(", ");
605 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
606 for sql in [
607 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
608 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
609 ] {
610 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
611 names.insert(row.id, row.name);
612 }
613 }
614 Ok(names)
615 }
616
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97617 /// `accounts`: the accounts behind these ids (at most 200), each with
618 /// its username and avatar, for lists that keep ids, such as who
619 /// starred a repository. Ids of no account are left out.
620 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
621 #[derive(serde::Deserialize)]
622 struct Row {
623 id: String,
624 username: String,
625 avatar: Option<String>,
626 }
627 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
628 let mut found = std::collections::HashMap::new();
629 if ids.is_empty() {
630 return Ok(found);
631 }
632 let marks = vec!["?"; ids.len()].join(", ");
633 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
634 let rows = self
635 .db
636 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
637 .bind(&bind)?
638 .all()
639 .await?
640 .results::<Row>()?;
641 for row in rows {
642 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
643 }
644 Ok(found)
645 }
646
API and MCP server, Rust identity service, registration, site redesign647 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
648 let rows = self
649 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca650 .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 redesign651 .bind(&[a.user.id.into()])?
652 .all()
653 .await?
654 .results::<KeyRow>()?;
655 Ok(rows.into_iter().map(SshKey::from).collect())
656 }
657
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge658 /// The account (user id) that registered each key, by fingerprint
659 /// (`SHA256:…`). At most 100; unknown keys are left out.
660 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
661 #[derive(serde::Deserialize)]
662 struct Row {
663 fingerprint: String,
664 user_id: String,
665 }
666 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
667 if fingerprints.is_empty() {
668 return Ok(std::collections::HashMap::new());
669 }
670 let marks = vec!["?"; fingerprints.len()].join(", ");
671 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
672 Ok(self
673 .db
674 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
675 .bind(&binds)?
676 .all()
677 .await?
678 .results::<Row>()?
679 .into_iter()
680 .map(|row| (row.fingerprint, row.user_id))
681 .collect())
682 }
683
API and MCP server, Rust identity service, registration, site redesign684 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
685 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
686 return Ok(Outcome::fail(
687 FailureCode::Invalid,
688 "That is not a valid OpenSSH public key.",
689 ));
690 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca691 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
692 if self.key_in_use(&key.fingerprint).await? {
693 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign694 }
695 let now = now_ms();
696 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
697 .into_iter()
698 .find(|candidate| !candidate.is_empty())
699 .unwrap_or_default()
700 .to_owned();
701 let row = KeyRow {
702 id: new_id("key", now),
703 title,
704 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos705 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca706 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign707 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca708 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign709 .prepare(
710 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
711 VALUES (?, ?, ?, ?, ?, ?)",
712 )
713 .bind(&[
714 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca715 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign716 row.title.as_str().into(),
717 key.public_key.into(),
718 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos719 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign720 ])?
721 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca722 .await;
723 // Added at the same moment elsewhere: the trigger or the unique
724 // index refused it.
725 if let Err(error) = inserted {
726 if self.key_in_use(&row.fingerprint).await? {
727 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
728 }
729 return Err(error);
730 }
Merge main (membership, two-factor, GitHub repo roles) into tokens731 let shown = format!("{} ({})", row.title, row.fingerprint);
732 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
733 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign734 Ok(Outcome::Ok(row.into()))
735 }
736
Merge main (membership, two-factor, GitHub repo roles) into tokens737 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca738 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens739 #[derive(Deserialize)]
740 struct Removed {
741 title: String,
742 fingerprint: String,
743 }
744 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca745 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens746 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca747 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens748 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca749 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens750 if let Some(removed) = removed {
751 let shown = format!("{} ({})", removed.title, removed.fingerprint);
752 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
753 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
754 }
API and MCP server, Rust identity service, registration, site redesign755 Ok(())
756 }
757}
758
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas759/// 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 member760/// 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)761/// and deleted workspaces and accounts past their restore window are purged
762/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas763#[event(scheduled)]
764async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
765 let Ok(db) = env.d1("DB") else { return };
766 let identity = Identity { db, env };
767 if let Err(error) = identity.notify_staff_of_requests().await {
768 worker::console_error!("waitlist summary: {error}");
769 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member770 if let Err(error) = identity.purge_due_workspaces().await {
771 worker::console_error!("workspace purge: {error}");
772 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)773 // And deleted accounts past theirs (account_deletion.rs).
774 if let Err(error) = identity.purge_due_accounts().await {
775 worker::console_error!("account purge: {error}");
776 }
Merge main (membership, two-factor, GitHub repo roles) into tokens777 // Once: creators of repositories made before they got Admin (members.rs).
778 if let Err(error) = identity.backfill_creator_grants().await {
779 worker::console_error!("creator grants: {error}");
780 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas781}
782
API and MCP server, Rust identity service, registration, site redesign783#[event(fetch)]
784async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
785 let Some(method) = rpc_method(&request) else {
786 return Response::error("Not found", 404);
787 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents788 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
789 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign790 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents791 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign792
Fast pages, required checks on the branch, self-hosted runners, honest incidents793 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette794 "register" => {
795 let outcome = identity.register(args(body)?).await?;
796 if let Outcome::Ok(signed_in) = &outcome {
797 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
798 }
799 reply(&outcome)
800 }
API and MCP server, Rust identity service, registration, site redesign801 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette802 "create_workspace" => {
803 let outcome = identity.create_workspace(args(body)?).await?;
804 if let Outcome::Ok(workspace) = &outcome {
805 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
806 }
807 reply(&outcome)
808 }
Workspaces own repositories809 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
810 "list_members" => reply(&identity.list_members(args(body)?).await?),
811 "add_member" => reply(&identity.add_member(args(body)?).await?),
812 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens813 // Owners, roles, leaving and member privileges; see members.rs.
814 "update_member" => reply(&identity.update_member(args(body)?).await?),
815 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
816 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
817 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
818 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
819 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette820 "update_workspace" => {
821 let outcome = identity.update_workspace(args(body)?).await?;
822 if let Outcome::Ok(workspace) = &outcome {
823 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
824 }
825 reply(&outcome)
826 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains827 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
828 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
829 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'830 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look831 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
832 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
833 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette834 "set_workspace_avatar" => {
835 let outcome = identity.set_workspace_avatar(args(body)?).await?;
836 if let Outcome::Ok(workspace) = &outcome {
837 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
838 }
839 reply(&outcome)
840 }
841 "set_user_avatar" => {
842 let a: SetUserAvatarArgs = args(body)?;
843 let (username, id) = (a.user.username.clone(), a.user.id.clone());
844 let outcome = identity.set_user_avatar(a).await?;
845 if matches!(outcome, Outcome::Ok(_)) {
846 identity.announce_user(&username, Some(&id)).await;
847 }
848 reply(&outcome)
849 }
Agents as a team: lifecycle, merge queue, billing and a new shell850 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
851 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
852 "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 look853 // Signing in with GitHub; see github.rs.
854 "github_enabled" => reply(&identity.github_enabled()),
855 "github_start" => reply(&identity.github_start(args(body)?).await?),
856 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
857 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
858 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
859 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
860 "github_account" => reply(&identity.github_account(args(body)?).await?),
861 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
862 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
863 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
864 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications865 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
866 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
867 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
868 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
869 "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 step870 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API871 "device_start" => reply(&identity.device_start(args(body)?).await?),
872 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
873 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
874 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning875 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
876 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)877 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
878 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning879 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
880 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look881 // A person's email addresses; see emails.rs and security.rs.
882 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
883 "add_email" => reply(&identity.add_email(args(body)?).await?),
884 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
885 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
886 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
887 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens888 // Two-factor authentication; see two_factor.rs.
889 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
890 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
891 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
892 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
893 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
894 "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 look895 "security_log" => reply(&identity.security_log(args(body)?).await?),
896 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
897 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
898 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
899 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
900 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign901 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
902 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
903 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
904 "user_for_access_token" => {
905 let a: TokenArgs = args(body)?;
906 reply(&identity.user_for_access_token(&a.token).await?)
907 }
908 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
909 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph910 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97911 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching912 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains913 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette914 "update_profile" => {
915 let outcome = identity.update_profile(args(body)?).await?;
916 if let Outcome::Ok(profile) = &outcome {
917 identity.announce_user(&profile.username, None).await;
918 }
919 reply(&outcome)
920 }
921 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains922 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign923 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge924 // Services only: who registered each key, for verifying commit
925 // signatures (repos' signatures.rs).
926 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign927 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca928 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
929 // A repository's deploy keys, and who an SSH key signs in as; see
930 // deploy_keys.rs.
931 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
932 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
933 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
934 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
935 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign936 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
937 "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 step938 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity939 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
940 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
941 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
942 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
943 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
944 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
945 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
946 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell947 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
948 "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 API949 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
950 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
951 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'952 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
953 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens954 "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 look955 // Invites and the waitlist; see invites.rs.
956 "registration" => reply(&identity.registration_mode()),
957 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
958 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
959 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
960 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
961 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
962 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)963 "list_invitations" => reply(&identity.list_invitations(args(body)?).await?),
964 "accept_invitation" => reply(&identity.accept_invitation(args(body)?).await?),
965 "decline_invitation" => reply(&identity.decline_invitation(args(body)?).await?),
966 "find_people" => reply(&identity.find_people(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look967 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
968 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
969 "request_access" => reply(&identity.request_access(args(body)?).await?),
970 // Who has access to a repository; see access.rs.
971 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
972 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
973 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
974 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
975 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
976 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
977 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
978 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
979 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'980 // Where a workspace keeps its repositories' git data (EU residency).
981 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
982 "set_workspace_residency" => reply(&identity.set_workspace_residency(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look983 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca984 "forget_repo_access" => {
985 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
986 // A purged repository's deploy keys go with its access.
987 identity.forget_deploy_keys(&a.repo_id).await?;
988 reply(&identity.forget_repo_access(a).await?)
989 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar990 // Teams (teams.rs).
991 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
992 "get_team" => reply(&identity.get_team(args(body)?).await?),
993 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'994 "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 bar995 "update_team" => reply(&identity.update_team(args(body)?).await?),
996 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
997 "team_members" => reply(&identity.team_members(args(body)?).await?),
998 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
999 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
1000 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
1001 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
1002 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
1003 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
1004 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1005 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1006 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1007 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1008 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1009 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1010 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1011 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1012 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1013 "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 quotas1014 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1015 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1016 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1017 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1018 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1019 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1020 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1021 // Shared invite links for a group; see shared_invites.rs.
1022 "admin_shared_invites" => reply(&identity.admin_shared_invites().await?),
1023 "admin_create_shared_invite" => reply(&identity.admin_create_shared_invite(args(body)?).await?),
1024 "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 member1025 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1026 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1027 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1028 "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)1029 // Deleting accounts (account_deletion.rs): the person from the
1030 // site, staff from sudo. There is no API route for it.
1031 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1032 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1033 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1034 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1035 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1036 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1037 // Workspace aliases, set by staff only; see aliases.rs.
1038 "admin_aliases" => reply(&identity.admin_aliases().await?),
1039 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1040 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1041 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1042 };
1043 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1044}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1045
1046#[cfg(test)]
1047mod register_tests {
1048 use super::*;
1049
1050 #[test]
1051 fn nobody_registers_as_g1t() {
1052 // What register checks the username with, whatever its case.
1053 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1054 assert_eq!(claimable_namespace(username), None, "{username}");
1055 }
1056 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1057 }
1058}

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