Skip to content
1,038 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server, Rust identity service, registration, site redesign1//! The identity service: accounts, sessions, SSH keys and access tokens.
2//!
3//! Reached only through service bindings; see `g1t_contracts::identity` for
4//! the methods and their arguments.
5
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6mod access;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'8mod aliases;
Workspace names and icons, and a component kit for every control9mod avatars;
API and MCP server, Rust identity service, registration, site redesign10mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11mod deletion;
Device sign-in replaces registering and minting tokens over the API12mod device;
Search across all of g1t, Explore, and a command palette13mod directory;
Email verification, password reset, and Git for AI scale positioning14mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15mod emails;
16mod github;
17mod invites;
Membership as GitHub has it: owners, roles, member privileges, 2FA18mod members;
OAuth 2.1 sign-in for MCP clients and other applications19mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person20mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21mod profiles;
22mod rename;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API23mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar25mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell27mod tokens;
Membership as GitHub has it: owners, roles, member privileges, 2FA28mod two_factor;
Workspaces own repositories29mod workspaces;
API and MCP server, Rust identity service, registration, site redesign30
31use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos32use 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-agent33use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign34use g1t_kit::{args, now_ms, reply, rpc_method};
35use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell36use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign37use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas38use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign39
RFC 3339 timestamps in identity and repos40const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
41const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
42const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign43const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning44const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
45
46/// A user as selected from the database; `verified` arrives as 0 or 1.
47#[derive(Deserialize)]
48struct Account {
49 id: String,
50 username: String,
51 verified: u8,
Workspace names and icons, and a component kit for every control52 /// Selected only where the person is being shown to themselves.
53 #[serde(default)]
54 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning55}
56
57impl From<Account> for User {
58 fn from(row: Account) -> Self {
59 User {
60 id: row.id,
61 username: row.username,
62 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control63 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell64 ..User::default()
Email verification, password reset, and Git for AI scale positioning65 }
66 }
67}
API and MCP server, Rust identity service, registration, site redesign68
69#[derive(Deserialize)]
70struct UserRow {
71 id: String,
72 username: String,
73 password_hash: String,
Email verification, password reset, and Git for AI scale positioning74 verified: u8,
API and MCP server, Rust identity service, registration, site redesign75}
76
Email verification, password reset, and Git for AI scale positioning77/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign78#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning79struct TokenOwner {
80 id: String,
81 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 /// The address a link was sent to; null on links from before accounts
83 /// had several, which are for the primary.
84 #[serde(default)]
85 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning86}
87
88#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign89struct KeyRow {
90 id: String,
91 title: String,
92 fingerprint: String,
RFC 3339 timestamps in identity and repos93 created_at: String,
API and MCP server, Rust identity service, registration, site redesign94}
95
96impl From<KeyRow> for SshKey {
97 fn from(row: KeyRow) -> Self {
98 SshKey {
99 id: row.id,
100 title: row.title,
101 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos102 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign103 }
104 }
105}
106
107struct Identity {
108 db: D1Database,
Email verification, password reset, and Git for AI scale positioning109 env: Env,
API and MCP server, Rust identity service, registration, site redesign110}
111
112impl Identity {
Workspaces own repositories113 /// Runs a query that returns at most one user, for showing to others:
114 /// without their workspaces.
115 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning116 Ok(self
117 .db
API and MCP server, Rust identity service, registration, site redesign118 .prepare(sql)
119 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning120 .first::<Account>(None)
121 .await?
122 .map(User::from))
123 }
124
Workspaces own repositories125 /// Attaches the workspaces a user belongs to, so that any service can
126 /// authorize them without asking again.
127 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
128 let Some(mut user) = user else {
129 return Ok(None);
130 };
Membership as GitHub has it: owners, roles, member privileges, 2FA131 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look132 // Roles on single repositories, under the same policy (access.rs).
133 let grants = self.grants_of(&user.id).await?;
Membership as GitHub has it: owners, roles, member privileges, 2FA134 // Access to a workspace is used only within its policy; see security.rs.
135 let within = self.within_policy(&user.id, memberships, grants).await?;
136 user.workspaces = within.memberships;
137 user.grants = within.grants;
138 user.held = within.held;
Workspaces own repositories139 Ok(Some(user))
140 }
141
142 /// Runs a query that resolves credentials to at most one user.
143 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
144 let user = self.find_public_user(sql, param).await?;
145 self.with_workspaces(user).await
146 }
147
Email verification, password reset, and Git for AI scale positioning148 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos149 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning150 let token = crypto::random_hex(32);
151 self.db
RFC 3339 timestamps in identity and repos152 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning153 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos154 VALUES (?, ?, ?, {})",
155 sql_after(ttl)
156 ))
Email verification, password reset, and Git for AI scale positioning157 .bind(&[
158 crypto::sha256_hex(&token).into(),
159 user_id.into(),
160 kind.into(),
161 ])?
162 .run()
163 .await?;
164 Ok(token)
API and MCP server, Rust identity service, registration, site redesign165 }
166
Email verification, password reset, and Git for AI scale positioning167 /// Consumes a token of `kind`, returning its owner if it was valid.
168 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
169 let id = crypto::sha256_hex(token);
170 let owner = self
171 .db
RFC 3339 timestamps in identity and repos172 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look173 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning174 JOIN users ON users.id = email_tokens.user_id
175 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos176 AND email_tokens.expires_at > {SQL_NOW}"
177 ))
Email verification, password reset, and Git for AI scale positioning178 .bind(&[id.as_str().into(), kind.into()])?
179 .first::<TokenOwner>(None)
180 .await?;
181 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look182 // Every outstanding token of this kind dies with the one used:
183 // every reset link, and every confirmation link for the same
184 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning185 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 .prepare(
187 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
188 AND (?2 = 'reset' OR email_id IS ?3)",
189 )
190 .bind(&[
191 owner.id.as_str().into(),
192 kind.into(),
193 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
194 ])?
Email verification, password reset, and Git for AI scale positioning195 .run()
196 .await?;
197 }
198 Ok(owner)
199 }
200
201 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
202 let token = self
203 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
204 .await?;
205 email::send_verification(&self.env, email, &user.username, &token).await
206 }
207
208 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 look209 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
210 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
211 }
212 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning213 }
214
215 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
216 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
217 return Ok(Outcome::fail(
218 FailureCode::Invalid,
219 "This confirmation link is not valid or has expired.",
220 ));
221 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look222 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
223 return Ok(Outcome::Fail(failure));
224 }
225 // Whether the account is confirmed: whether its primary is.
226 let verified = self
227 .find_public_user(
228 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
229 &owner.id,
230 )
231 .await?
232 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning233 Ok(Outcome::Ok(User {
234 id: owner.id,
235 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 verified,
Workspaces own repositories237 ..User::default()
Email verification, password reset, and Git for AI scale positioning238 }))
239 }
240
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241 /// Any confirmed address of an account can ask for a reset; so can the
242 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning243 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 look244 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
245 && match a.client.as_deref() {
246 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
247 None => true,
248 };
249 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 exists250 // A failure from here on happens only for a real account, so it
251 // is logged, never answered: the reply below stays the same.
252 if let Err(error) = self.send_reset(&target).await {
253 worker::console_error!("password reset for a known address failed: {error}");
254 }
255 }
256 // The same answer either way, so addresses cannot be probed.
257 Ok(true)
258 }
259
260 /// Saves a reset link for `target` and mails it, telling the account's
261 /// other addresses.
262 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
263 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look264 let token = crypto::random_hex(32);
265 self.db
266 .prepare(format!(
267 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
268 VALUES (?, ?, 'reset', {}, ?)",
269 sql_after(RESET_TTL_SECONDS)
270 ))
271 .bind(&[
272 crypto::sha256_hex(&token).into(),
273 target.user_id.as_str().into(),
274 target.email_id.as_str().into(),
275 ])?
276 .run()
Email verification, password reset, and Git for AI scale positioning277 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
279 // The primary and the backup hear of it when it went elsewhere.
280 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
281 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
282 let change = format!("A password reset was asked for through {}", target.display);
283 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
284 worker::console_error!("security notice failed: {error}");
285 }
286 }
Email verification, password reset, and Git for AI scale positioning287 }
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 exists288 Ok(())
Email verification, password reset, and Git for AI scale positioning289 }
290
291 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
292 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
293 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
294 }
295 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
296 return Ok(Outcome::fail(
297 FailureCode::Invalid,
298 "This reset link is not valid or has expired.",
299 ));
300 };
301 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look302 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning303 .bind(&[
304 crypto::hash_password(&a.password).into(),
305 owner.id.as_str().into(),
306 ])?
307 .run()
308 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look309 // Following an emailed link also proves the address it went to
310 // (unless another account confirmed it first).
311 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
312 // Anyone signed in with the old password is signed out, and nobody
313 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning314 self.db
315 .prepare("DELETE FROM sessions WHERE user_id = ?")
316 .bind(&[owner.id.as_str().into()])?
317 .run()
318 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
320 self.log_security(&owner.id, "password_changed", None, None).await;
321 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
322 let verified = self
323 .find_public_user(
324 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
325 &owner.id,
326 )
327 .await?
328 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning329 Ok(Outcome::Ok(User {
330 id: owner.id,
331 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look332 verified,
Workspaces own repositories333 ..User::default()
Email verification, password reset, and Git for AI scale positioning334 }))
335 }
336
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look337 /// The account a login names: a username, or any confirmed address.
338 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
339 let login = login.trim().to_lowercase();
340 let (column, value) = if login.contains('@') {
341 match self.user_with_verified_email(&login).await? {
342 Some(id) => ("id", id),
343 None => return Ok(None),
344 }
345 } else {
346 ("username", login)
347 };
348 self.db
349 .prepare(format!(
350 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
351 ))
352 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign353 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look354 .await
355 }
356
357 /// Checks a password for a login, throttled (see throttle.rs). The
358 /// refusal is one of two messages, the same for every account.
359 async fn checked_password(
360 &self,
361 login: &str,
362 password: &str,
363 client: Option<&str>,
364 ) -> Result<std::result::Result<User, &'static str>> {
365 let row = self.password_row(login).await?;
366 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
367 let (account_key, client_key) = Identity::password_keys(&subject, client);
368 if self.password_locked(&account_key, client_key.as_deref()).await? {
369 return Ok(Err(throttle::THROTTLED));
370 }
371 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
372 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
373 Some(row) => {
374 self.clear(&account_key).await?;
375 Ok(Ok(User {
376 id: row.id,
377 username: row.username,
378 verified: row.verified != 0,
379 ..User::default()
380 }))
381 }
382 None => {
383 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
384 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
385 Ok(Err("Incorrect username or password."))
386 }
387 }
388 }
389
Membership as GitHub has it: owners, roles, member privileges, 2FA390 /// Git over HTTPS with the account's password. With two-factor
391 /// authentication on, a password alone is never enough: use an access
392 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look393 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
394 let user = self.checked_password(login, password, None).await?.ok();
Membership as GitHub has it: owners, roles, member privileges, 2FA395 if let Some(user) = &user
396 && self.two_factor_enabled(&user.id).await?
397 {
398 return Ok(None);
399 }
Workspaces own repositories400 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign401 }
402
403 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
404 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-agent405 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign406 let email = a.email.trim().to_lowercase();
407 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 look408 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
409 // The invite first: without one, nothing else on the form matters.
410 if self.invites_required() && invite_code.is_none() {
411 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
412 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent413 if !claimable {
API and MCP server, Rust identity service, registration, site redesign414 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent415 "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 redesign416 );
417 }
418 let well_formed_email = email
419 .split_once('@')
420 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
421 && !email.contains(char::is_whitespace);
422 if !well_formed_email {
423 return invalid("Enter a valid email address.");
424 }
425 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning426 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign427 }
428 let taken = self
429 .db
Agents as a team: lifecycle, merge queue, billing and a new shell430 // Usernames and workspaces share one namespace, so that a name
431 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look432 // An address is taken once an account has confirmed it; an
433 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell434 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look435 "SELECT username FROM users WHERE username = ?
436 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 shell437 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
438 )
439 .bind(&[
440 username.as_str().into(),
441 email.as_str().into(),
442 username.as_str().into(),
443 ])?
API and MCP server, Rust identity service, registration, site redesign444 .first::<serde_json::Value>(None)
445 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look446 // A renamed workspace's old slug stays reserved for it a while, and
447 // a deleted workspace's for good.
448 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign449 return Ok(Outcome::fail(
450 FailureCode::Conflict,
451 "That username or email is already registered.",
452 ));
453 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look454 let password_hash = crypto::hash_password(&a.password);
455 let user = match self
456 .create_account(invites::NewAccount {
457 username: &username,
458 email: &email,
459 password_hash: &password_hash,
460 verified: false,
461 invite_code,
462 client: a.client.as_deref(),
463 })
464 .await?
465 {
466 Outcome::Ok(user) => user,
467 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign468 };
Email verification, password reset, and Git for AI scale positioning469 // The account exists either way; the email can be sent again later.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas470 // An invite sent to this address confirmed it already (invites.rs).
471 if !user.verified
472 && let Err(error) = self.send_verification(&user, &email).await
473 {
Email verification, password reset, and Git for AI scale positioning474 worker::console_error!("verification email failed: {error}");
475 }
API and MCP server, Rust identity service, registration, site redesign476 self.start_session(user).await
477 }
478
479 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 look480 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
481 Ok(user) => user,
482 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign483 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look484 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign485 self.start_session(user).await
486 }
487
Membership as GitHub has it: owners, roles, member privileges, 2FA488 /// Starts a session for someone who just proved their password (or
489 /// GitHub account). With two-factor authentication on, it starts none:
490 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign491 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Membership as GitHub has it: owners, roles, member privileges, 2FA492 if self.two_factor_enabled(&user.id).await? {
493 let challenge = self.issue_challenge(&user.id).await?;
494 return Ok(Outcome::Ok(SignedIn {
495 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
496 session_token: String::new(),
497 two_factor_challenge: Some(challenge),
498 }));
499 }
500 self.session_for(user).await
501 }
502
503 /// A new session for `user`, who has proved who they are in full.
504 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
API and MCP server, Rust identity service, registration, site redesign505 let session_token = crypto::random_hex(32);
506 self.db
RFC 3339 timestamps in identity and repos507 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look508 // Signing in is proof it is the person: see security.rs.
509 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos510 sql_after(SESSION_TTL_SECONDS)
511 ))
API and MCP server, Rust identity service, registration, site redesign512 .bind(&[
513 crypto::sha256_hex(&session_token).into(),
514 user.id.as_str().into(),
515 ])?
516 .run()
517 .await?;
518 Ok(Outcome::Ok(SignedIn {
519 user,
520 session_token,
Membership as GitHub has it: owners, roles, member privileges, 2FA521 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign522 }))
523 }
524
525 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
526 self.db
527 .prepare("DELETE FROM sessions WHERE id = ?")
528 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
529 .run()
530 .await?;
531 Ok(())
532 }
533
534 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
535 self.find_user(
RFC 3339 timestamps in identity and repos536 &format!(
Workspace names and icons, and a component kit for every control537 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
538 users.avatar
RFC 3339 timestamps in identity and repos539 FROM sessions JOIN users ON users.id = sessions.user_id
540 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
541 ),
API and MCP server, Rust identity service, registration, site redesign542 &crypto::sha256_hex(&a.session_token),
543 )
544 .await
545 }
546
547 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
548 // Like GitHub, a token alone identifies its user.
549 if a.secret.starts_with(TOKEN_PREFIX) {
550 self.user_for_access_token(&a.secret).await
551 } else {
552 self.user_for_password(&a.username, &a.secret).await
553 }
554 }
555
556 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
557 self.find_user(
Email verification, password reset, and Git for AI scale positioning558 "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 redesign559 JOIN users ON users.id = ssh_keys.user_id
560 WHERE fingerprint = ?",
561 &a.fingerprint,
562 )
563 .await
564 }
565
566 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories567 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning568 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign569 &a.username.to_lowercase(),
570 )
571 .await
572 }
573
Inbox: threads, reasons, subscriptions and watching574 /// `notify_by_email`: an inbox item, emailed to the person it is for,
575 /// only at a confirmed address and only while they can still read the
576 /// repository it is about. Returns whether it was sent.
577 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
578 #[derive(Deserialize)]
579 struct Address {
580 email: Option<String>,
581 }
582 let user = self
583 .find_user(
584 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
585 &a.username.to_lowercase(),
586 )
587 .await?;
588 let Some(user) = user.filter(|user| user.verified) else {
589 return Ok(false);
590 };
591 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
592 &self.env.service("REPOS")?,
593 "readable",
594 &g1t_contracts::repos::ReadableArgs {
595 ids: vec![a.repo_id.clone()],
596 viewer: Some(user.clone()),
597 },
598 )
599 .await?;
600 if readable.is_empty() {
601 return Ok(false);
602 }
603 let address = self
604 .db
605 .prepare("SELECT email FROM users WHERE id = ?")
606 .bind(&[user.id.as_str().into()])?
607 .first::<Address>(None)
608 .await?
609 .and_then(|row| row.email)
610 .filter(|email| !email.trim().is_empty());
611 let Some(address) = address else {
612 return Ok(false);
613 };
614 email::send_notification(&self.env, &address, &a).await?;
615 Ok(true)
616 }
617
What happened across an outcome, as a feed beside its graph618 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
619 #[derive(serde::Deserialize)]
620 struct Named {
621 id: String,
622 name: String,
623 }
624 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
625 let mut names = std::collections::HashMap::new();
626 if ids.is_empty() {
627 return Ok(names);
628 }
629 let marks = vec!["?"; ids.len()].join(", ");
630 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
631 for sql in [
632 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
633 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
634 ] {
635 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
636 names.insert(row.id, row.name);
637 }
638 }
639 Ok(names)
640 }
641
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97642 /// `accounts`: the accounts behind these ids (at most 200), each with
643 /// its username and avatar, for lists that keep ids, such as who
644 /// starred a repository. Ids of no account are left out.
645 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
646 #[derive(serde::Deserialize)]
647 struct Row {
648 id: String,
649 username: String,
650 avatar: Option<String>,
651 }
652 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
653 let mut found = std::collections::HashMap::new();
654 if ids.is_empty() {
655 return Ok(found);
656 }
657 let marks = vec!["?"; ids.len()].join(", ");
658 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
659 let rows = self
660 .db
661 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
662 .bind(&bind)?
663 .all()
664 .await?
665 .results::<Row>()?;
666 for row in rows {
667 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
668 }
669 Ok(found)
670 }
671
API and MCP server, Rust identity service, registration, site redesign672 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
673 let rows = self
674 .db
675 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
676 .bind(&[a.user.id.into()])?
677 .all()
678 .await?
679 .results::<KeyRow>()?;
680 Ok(rows.into_iter().map(SshKey::from).collect())
681 }
682
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge683 /// The account (user id) that registered each key, by fingerprint
684 /// (`SHA256:…`). At most 100; unknown keys are left out.
685 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
686 #[derive(serde::Deserialize)]
687 struct Row {
688 fingerprint: String,
689 user_id: String,
690 }
691 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
692 if fingerprints.is_empty() {
693 return Ok(std::collections::HashMap::new());
694 }
695 let marks = vec!["?"; fingerprints.len()].join(", ");
696 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
697 Ok(self
698 .db
699 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
700 .bind(&binds)?
701 .all()
702 .await?
703 .results::<Row>()?
704 .into_iter()
705 .map(|row| (row.fingerprint, row.user_id))
706 .collect())
707 }
708
API and MCP server, Rust identity service, registration, site redesign709 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
710 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
711 return Ok(Outcome::fail(
712 FailureCode::Invalid,
713 "That is not a valid OpenSSH public key.",
714 ));
715 };
716 let taken = self
717 .db
718 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
719 .bind(&[key.fingerprint.as_str().into()])?
720 .first::<serde_json::Value>(None)
721 .await?;
722 if taken.is_some() {
723 return Ok(Outcome::fail(
724 FailureCode::Conflict,
725 "That key is already registered.",
726 ));
727 }
728 let now = now_ms();
729 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
730 .into_iter()
731 .find(|candidate| !candidate.is_empty())
732 .unwrap_or_default()
733 .to_owned();
734 let row = KeyRow {
735 id: new_id("key", now),
736 title,
737 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos738 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign739 };
740 self.db
741 .prepare(
742 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
743 VALUES (?, ?, ?, ?, ?, ?)",
744 )
745 .bind(&[
746 row.id.as_str().into(),
Membership as GitHub has it: owners, roles, member privileges, 2FA747 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign748 row.title.as_str().into(),
749 key.public_key.into(),
750 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos751 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign752 ])?
753 .run()
754 .await?;
Membership as GitHub has it: owners, roles, member privileges, 2FA755 let shown = format!("{} ({})", row.title, row.fingerprint);
756 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
757 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign758 Ok(Outcome::Ok(row.into()))
759 }
760
Membership as GitHub has it: owners, roles, member privileges, 2FA761 /// Deletes one of the person's SSH keys.
762 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
763 #[derive(Deserialize)]
764 struct Removed {
765 title: String,
766 fingerprint: String,
767 }
768 let removed = self
769 .db
770 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
771 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
772 .first::<Removed>(None)
API and MCP server, Rust identity service, registration, site redesign773 .await?;
Membership as GitHub has it: owners, roles, member privileges, 2FA774 if let Some(removed) = removed {
775 let shown = format!("{} ({})", removed.title, removed.fingerprint);
776 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
777 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
778 }
API and MCP server, Rust identity service, registration, site redesign779 Ok(())
780 }
781}
782
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas783/// 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 member784/// the last summary's window was still open, so none waits on a later one;
785/// and deleted workspaces past their restore window are purged
786/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas787#[event(scheduled)]
788async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
789 let Ok(db) = env.d1("DB") else { return };
790 let identity = Identity { db, env };
791 if let Err(error) = identity.notify_staff_of_requests().await {
792 worker::console_error!("waitlist summary: {error}");
793 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member794 if let Err(error) = identity.purge_due_workspaces().await {
795 worker::console_error!("workspace purge: {error}");
796 }
Membership as GitHub has it: owners, roles, member privileges, 2FA797 // Once: creators of repositories made before they got Admin (members.rs).
798 if let Err(error) = identity.backfill_creator_grants().await {
799 worker::console_error!("creator grants: {error}");
800 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas801}
802
API and MCP server, Rust identity service, registration, site redesign803#[event(fetch)]
804async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
805 let Some(method) = rpc_method(&request) else {
806 return Response::error("Not found", 404);
807 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents808 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
809 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign810 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents811 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign812
Fast pages, required checks on the branch, self-hosted runners, honest incidents813 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette814 "register" => {
815 let outcome = identity.register(args(body)?).await?;
816 if let Outcome::Ok(signed_in) = &outcome {
817 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
818 }
819 reply(&outcome)
820 }
API and MCP server, Rust identity service, registration, site redesign821 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette822 "create_workspace" => {
823 let outcome = identity.create_workspace(args(body)?).await?;
824 if let Outcome::Ok(workspace) = &outcome {
825 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
826 }
827 reply(&outcome)
828 }
Workspaces own repositories829 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
830 "list_members" => reply(&identity.list_members(args(body)?).await?),
831 "add_member" => reply(&identity.add_member(args(body)?).await?),
832 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Membership as GitHub has it: owners, roles, member privileges, 2FA833 // Owners, roles, leaving and member privileges; see members.rs.
834 "update_member" => reply(&identity.update_member(args(body)?).await?),
835 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
836 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
837 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
838 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
839 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette840 "update_workspace" => {
841 let outcome = identity.update_workspace(args(body)?).await?;
842 if let Outcome::Ok(workspace) = &outcome {
843 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
844 }
845 reply(&outcome)
846 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains847 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
848 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
849 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'850 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look851 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
852 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
853 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette854 "set_workspace_avatar" => {
855 let outcome = identity.set_workspace_avatar(args(body)?).await?;
856 if let Outcome::Ok(workspace) = &outcome {
857 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
858 }
859 reply(&outcome)
860 }
861 "set_user_avatar" => {
862 let a: SetUserAvatarArgs = args(body)?;
863 let (username, id) = (a.user.username.clone(), a.user.id.clone());
864 let outcome = identity.set_user_avatar(a).await?;
865 if matches!(outcome, Outcome::Ok(_)) {
866 identity.announce_user(&username, Some(&id)).await;
867 }
868 reply(&outcome)
869 }
Agents as a team: lifecycle, merge queue, billing and a new shell870 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
871 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
872 "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 look873 // Signing in with GitHub; see github.rs.
874 "github_enabled" => reply(&identity.github_enabled()),
875 "github_start" => reply(&identity.github_start(args(body)?).await?),
876 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
877 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
878 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
879 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
880 "github_account" => reply(&identity.github_account(args(body)?).await?),
881 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
882 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
883 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
884 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications885 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
886 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
887 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
888 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
889 "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 step890 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API891 "device_start" => reply(&identity.device_start(args(body)?).await?),
892 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
893 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
894 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning895 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
896 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
897 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
898 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look899 // A person's email addresses; see emails.rs and security.rs.
900 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
901 "add_email" => reply(&identity.add_email(args(body)?).await?),
902 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
903 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
904 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
905 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Membership as GitHub has it: owners, roles, member privileges, 2FA906 // Two-factor authentication; see two_factor.rs.
907 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
908 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
909 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
910 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
911 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
912 "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 look913 "security_log" => reply(&identity.security_log(args(body)?).await?),
914 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
915 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
916 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
917 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
918 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign919 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
920 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
921 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
922 "user_for_access_token" => {
923 let a: TokenArgs = args(body)?;
924 reply(&identity.user_for_access_token(&a.token).await?)
925 }
926 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
927 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph928 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97929 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching930 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains931 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette932 "update_profile" => {
933 let outcome = identity.update_profile(args(body)?).await?;
934 if let Outcome::Ok(profile) = &outcome {
935 identity.announce_user(&profile.username, None).await;
936 }
937 reply(&outcome)
938 }
939 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains940 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign941 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge942 // Services only: who registered each key, for verifying commit
943 // signatures (repos' signatures.rs).
944 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign945 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Membership as GitHub has it: owners, roles, member privileges, 2FA946 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign947 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
948 "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 step949 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell950 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
951 "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 API952 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
953 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
954 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Membership as GitHub has it: owners, roles, member privileges, 2FA955 "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 look956 // Invites and the waitlist; see invites.rs.
957 "registration" => reply(&identity.registration_mode()),
958 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
959 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
960 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
961 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
962 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
963 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
964 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
965 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
966 "request_access" => reply(&identity.request_access(args(body)?).await?),
967 // Who has access to a repository; see access.rs.
968 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
969 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
970 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
971 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
972 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
973 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
974 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
975 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
976 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'977 // Where a workspace keeps its repositories' git data (EU residency).
978 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
979 "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 look980 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
981 "forget_repo_access" => reply(&identity.forget_repo_access(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar982 // Teams (teams.rs).
983 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
984 "get_team" => reply(&identity.get_team(args(body)?).await?),
985 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'986 "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 bar987 "update_team" => reply(&identity.update_team(args(body)?).await?),
988 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
989 "team_members" => reply(&identity.team_members(args(body)?).await?),
990 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
991 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
992 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
993 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
994 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
995 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
996 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
997 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
998 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
999 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1000 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1001 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1002 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1003 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1004 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1005 "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 quotas1006 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1007 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1008 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1009 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1010 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1011 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1012 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1013 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1014 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1015 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1016 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1017 // Workspace aliases, set by staff only; see aliases.rs.
1018 "admin_aliases" => reply(&identity.admin_aliases().await?),
1019 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1020 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1021 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1022 };
1023 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1024}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1025
1026#[cfg(test)]
1027mod register_tests {
1028 use super::*;
1029
1030 #[test]
1031 fn nobody_registers_as_g1t() {
1032 // What register checks the username with, whatever its case.
1033 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1034 assert_eq!(claimable_namespace(username), None, "{username}");
1035 }
1036 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1037 }
1038}

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