Skip to content
1,078 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
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell616 /// `users_for_audience`: the people behind these ids (at most 50), each
617 /// with their workspaces, roles, base permissions and repository grants,
618 /// under each workspace's policy, as a signed-in viewer would have them.
619 /// For the agents service, which answers only with what every person
620 /// who will read the answer may see (docs/WORKSPACE.md, "What an agent
621 /// can and can't know"). Ids of no live account are left out, so the
622 /// caller can tell someone it could not resolve. Reached only by service
623 /// binding.
624 async fn users_for_audience(&self, a: UsernamesArgs) -> Result<Vec<User>> {
625 let mut found = Vec::new();
626 for id in a.ids.iter().take(50) {
627 let user = self
628 .find_user(
629 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ? AND deleted_at IS NULL",
630 id,
631 )
632 .await?;
633 if let Some(user) = user {
634 found.push(user);
635 }
636 }
637 Ok(found)
638 }
639
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97640 /// `accounts`: the accounts behind these ids (at most 200), each with
641 /// its username and avatar, for lists that keep ids, such as who
642 /// starred a repository. Ids of no account are left out.
643 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
644 #[derive(serde::Deserialize)]
645 struct Row {
646 id: String,
647 username: String,
648 avatar: Option<String>,
649 }
650 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
651 let mut found = std::collections::HashMap::new();
652 if ids.is_empty() {
653 return Ok(found);
654 }
655 let marks = vec!["?"; ids.len()].join(", ");
656 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
657 let rows = self
658 .db
659 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
660 .bind(&bind)?
661 .all()
662 .await?
663 .results::<Row>()?;
664 for row in rows {
665 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
666 }
667 Ok(found)
668 }
669
API and MCP server, Rust identity service, registration, site redesign670 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
671 let rows = self
672 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca673 .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 redesign674 .bind(&[a.user.id.into()])?
675 .all()
676 .await?
677 .results::<KeyRow>()?;
678 Ok(rows.into_iter().map(SshKey::from).collect())
679 }
680
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge681 /// The account (user id) that registered each key, by fingerprint
682 /// (`SHA256:…`). At most 100; unknown keys are left out.
683 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
684 #[derive(serde::Deserialize)]
685 struct Row {
686 fingerprint: String,
687 user_id: String,
688 }
689 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
690 if fingerprints.is_empty() {
691 return Ok(std::collections::HashMap::new());
692 }
693 let marks = vec!["?"; fingerprints.len()].join(", ");
694 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
695 Ok(self
696 .db
697 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
698 .bind(&binds)?
699 .all()
700 .await?
701 .results::<Row>()?
702 .into_iter()
703 .map(|row| (row.fingerprint, row.user_id))
704 .collect())
705 }
706
API and MCP server, Rust identity service, registration, site redesign707 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
708 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
709 return Ok(Outcome::fail(
710 FailureCode::Invalid,
711 "That is not a valid OpenSSH public key.",
712 ));
713 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca714 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
715 if self.key_in_use(&key.fingerprint).await? {
716 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign717 }
718 let now = now_ms();
719 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
720 .into_iter()
721 .find(|candidate| !candidate.is_empty())
722 .unwrap_or_default()
723 .to_owned();
724 let row = KeyRow {
725 id: new_id("key", now),
726 title,
727 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos728 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca729 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign730 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca731 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign732 .prepare(
733 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
734 VALUES (?, ?, ?, ?, ?, ?)",
735 )
736 .bind(&[
737 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca738 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign739 row.title.as_str().into(),
740 key.public_key.into(),
741 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos742 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign743 ])?
744 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca745 .await;
746 // Added at the same moment elsewhere: the trigger or the unique
747 // index refused it.
748 if let Err(error) = inserted {
749 if self.key_in_use(&row.fingerprint).await? {
750 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
751 }
752 return Err(error);
753 }
Merge main (membership, two-factor, GitHub repo roles) into tokens754 let shown = format!("{} ({})", row.title, row.fingerprint);
755 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
756 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign757 Ok(Outcome::Ok(row.into()))
758 }
759
Merge main (membership, two-factor, GitHub repo roles) into tokens760 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca761 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens762 #[derive(Deserialize)]
763 struct Removed {
764 title: String,
765 fingerprint: String,
766 }
767 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca768 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens769 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca770 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens771 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca772 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens773 if let Some(removed) = removed {
774 let shown = format!("{} ({})", removed.title, removed.fingerprint);
775 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
776 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
777 }
API and MCP server, Rust identity service, registration, site redesign778 Ok(())
779 }
780}
781
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas782/// 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 member783/// 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)784/// and deleted workspaces and accounts past their restore window are purged
785/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas786#[event(scheduled)]
787async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
788 let Ok(db) = env.d1("DB") else { return };
789 let identity = Identity { db, env };
790 if let Err(error) = identity.notify_staff_of_requests().await {
791 worker::console_error!("waitlist summary: {error}");
792 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member793 if let Err(error) = identity.purge_due_workspaces().await {
794 worker::console_error!("workspace purge: {error}");
795 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)796 // And deleted accounts past theirs (account_deletion.rs).
797 if let Err(error) = identity.purge_due_accounts().await {
798 worker::console_error!("account purge: {error}");
799 }
Merge main (membership, two-factor, GitHub repo roles) into tokens800 // Once: creators of repositories made before they got Admin (members.rs).
801 if let Err(error) = identity.backfill_creator_grants().await {
802 worker::console_error!("creator grants: {error}");
803 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas804}
805
API and MCP server, Rust identity service, registration, site redesign806#[event(fetch)]
807async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
808 let Some(method) = rpc_method(&request) else {
809 return Response::error("Not found", 404);
810 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents811 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
812 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign813 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents814 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign815
Fast pages, required checks on the branch, self-hosted runners, honest incidents816 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette817 "register" => {
818 let outcome = identity.register(args(body)?).await?;
819 if let Outcome::Ok(signed_in) = &outcome {
820 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
821 }
822 reply(&outcome)
823 }
API and MCP server, Rust identity service, registration, site redesign824 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette825 "create_workspace" => {
826 let outcome = identity.create_workspace(args(body)?).await?;
827 if let Outcome::Ok(workspace) = &outcome {
828 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
829 }
830 reply(&outcome)
831 }
Workspaces own repositories832 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
833 "list_members" => reply(&identity.list_members(args(body)?).await?),
834 "add_member" => reply(&identity.add_member(args(body)?).await?),
835 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens836 // Owners, roles, leaving and member privileges; see members.rs.
837 "update_member" => reply(&identity.update_member(args(body)?).await?),
838 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
839 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
840 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
841 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
842 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette843 "update_workspace" => {
844 let outcome = identity.update_workspace(args(body)?).await?;
845 if let Outcome::Ok(workspace) = &outcome {
846 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
847 }
848 reply(&outcome)
849 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains850 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
851 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
852 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'853 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look854 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
855 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
856 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette857 "set_workspace_avatar" => {
858 let outcome = identity.set_workspace_avatar(args(body)?).await?;
859 if let Outcome::Ok(workspace) = &outcome {
860 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
861 }
862 reply(&outcome)
863 }
864 "set_user_avatar" => {
865 let a: SetUserAvatarArgs = args(body)?;
866 let (username, id) = (a.user.username.clone(), a.user.id.clone());
867 let outcome = identity.set_user_avatar(a).await?;
868 if matches!(outcome, Outcome::Ok(_)) {
869 identity.announce_user(&username, Some(&id)).await;
870 }
871 reply(&outcome)
872 }
Agents as a team: lifecycle, merge queue, billing and a new shell873 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
874 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
875 "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 look876 // Signing in with GitHub; see github.rs.
877 "github_enabled" => reply(&identity.github_enabled()),
878 "github_start" => reply(&identity.github_start(args(body)?).await?),
879 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
880 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
881 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
882 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
883 "github_account" => reply(&identity.github_account(args(body)?).await?),
884 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
885 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
886 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
887 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications888 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
889 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
890 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
891 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
892 "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 step893 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API894 "device_start" => reply(&identity.device_start(args(body)?).await?),
895 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
896 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
897 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning898 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
899 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)900 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
901 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning902 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
903 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look904 // A person's email addresses; see emails.rs and security.rs.
905 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
906 "add_email" => reply(&identity.add_email(args(body)?).await?),
907 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
908 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
909 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
910 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens911 // Two-factor authentication; see two_factor.rs.
912 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
913 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
914 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
915 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
916 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
917 "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 look918 "security_log" => reply(&identity.security_log(args(body)?).await?),
919 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
920 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
921 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
922 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
923 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign924 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
925 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
926 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
927 "user_for_access_token" => {
928 let a: TokenArgs = args(body)?;
929 reply(&identity.user_for_access_token(&a.token).await?)
930 }
931 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
932 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph933 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97934 "accounts" => reply(&identity.accounts(args(body)?).await?),
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell935 "users_for_audience" => reply(&identity.users_for_audience(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching936 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains937 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette938 "update_profile" => {
939 let outcome = identity.update_profile(args(body)?).await?;
940 if let Outcome::Ok(profile) = &outcome {
941 identity.announce_user(&profile.username, None).await;
942 }
943 reply(&outcome)
944 }
945 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains946 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign947 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge948 // Services only: who registered each key, for verifying commit
949 // signatures (repos' signatures.rs).
950 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign951 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca952 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
953 // A repository's deploy keys, and who an SSH key signs in as; see
954 // deploy_keys.rs.
955 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
956 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
957 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
958 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
959 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign960 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
961 "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 step962 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity963 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
964 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
965 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
966 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
967 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
968 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
969 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
970 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell971 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
972 "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 API973 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
974 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
975 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'976 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
977 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens978 "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 look979 // Invites and the waitlist; see invites.rs.
980 "registration" => reply(&identity.registration_mode()),
981 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
982 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
983 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
984 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
985 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
986 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
987 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
988 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
989 "request_access" => reply(&identity.request_access(args(body)?).await?),
990 // Who has access to a repository; see access.rs.
991 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
992 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
993 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
994 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
995 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
996 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
997 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
998 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
999 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'1000 // Where a workspace keeps its repositories' git data (EU residency).
1001 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
1002 "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 look1003 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1004 "forget_repo_access" => {
1005 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
1006 // A purged repository's deploy keys go with its access.
1007 identity.forget_deploy_keys(&a.repo_id).await?;
1008 reply(&identity.forget_repo_access(a).await?)
1009 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1010 // Teams (teams.rs).
1011 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
1012 "get_team" => reply(&identity.get_team(args(body)?).await?),
1013 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1014 "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 bar1015 "update_team" => reply(&identity.update_team(args(body)?).await?),
1016 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
1017 "team_members" => reply(&identity.team_members(args(body)?).await?),
1018 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
1019 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
1020 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
1021 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
1022 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
1023 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
1024 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1025 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1026 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1027 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1028 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1029 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1030 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1031 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1032 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1033 "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 quotas1034 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1035 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1036 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1037 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1038 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1039 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1040 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1041 // Shared invite links for a group; see shared_invites.rs.
1042 "admin_shared_invites" => reply(&identity.admin_shared_invites().await?),
1043 "admin_create_shared_invite" => reply(&identity.admin_create_shared_invite(args(body)?).await?),
1044 "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 member1045 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1046 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1047 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1048 "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)1049 // Deleting accounts (account_deletion.rs): the person from the
1050 // site, staff from sudo. There is no API route for it.
1051 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1052 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1053 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1054 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1055 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1056 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1057 // Workspace aliases, set by staff only; see aliases.rs.
1058 "admin_aliases" => reply(&identity.admin_aliases().await?),
1059 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1060 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1061 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1062 };
1063 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1064}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1065
1066#[cfg(test)]
1067mod register_tests {
1068 use super::*;
1069
1070 #[test]
1071 fn nobody_registers_as_g1t() {
1072 // What register checks the username with, whatever its case.
1073 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1074 assert_eq!(claimable_namespace(username), None, "{username}");
1075 }
1076 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1077 }
1078}

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