Skip to content

g1t/services/identity/src/lib.rs

946 lines42,504 bytesCodeBlame

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;
OAuth 2.1 sign-in for MCP clients and other applications18mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person19mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains20mod profiles;
21mod rename;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API22mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar24mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell26mod tokens;
Workspaces own repositories27mod workspaces;
API and MCP server, Rust identity service, registration, site redesign28
29use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos30use 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-agent31use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign32use g1t_kit::{args, now_ms, reply, rpc_method};
33use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell34use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign35use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas36use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign37
RFC 3339 timestamps in identity and repos38const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
39const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
40const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign41const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning42const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
43
44/// A user as selected from the database; `verified` arrives as 0 or 1.
45#[derive(Deserialize)]
46struct Account {
47 id: String,
48 username: String,
49 verified: u8,
Workspace names and icons, and a component kit for every control50 /// Selected only where the person is being shown to themselves.
51 #[serde(default)]
52 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning53}
54
55impl From<Account> for User {
56 fn from(row: Account) -> Self {
57 User {
58 id: row.id,
59 username: row.username,
60 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control61 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell62 ..User::default()
Email verification, password reset, and Git for AI scale positioning63 }
64 }
65}
API and MCP server, Rust identity service, registration, site redesign66
67#[derive(Deserialize)]
68struct UserRow {
69 id: String,
70 username: String,
71 password_hash: String,
Email verification, password reset, and Git for AI scale positioning72 verified: u8,
API and MCP server, Rust identity service, registration, site redesign73}
74
Email verification, password reset, and Git for AI scale positioning75/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign76#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning77struct TokenOwner {
78 id: String,
79 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look80 /// The address a link was sent to; null on links from before accounts
81 /// had several, which are for the primary.
82 #[serde(default)]
83 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning84}
85
86#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign87struct KeyRow {
88 id: String,
89 title: String,
90 fingerprint: String,
RFC 3339 timestamps in identity and repos91 created_at: String,
API and MCP server, Rust identity service, registration, site redesign92}
93
94impl From<KeyRow> for SshKey {
95 fn from(row: KeyRow) -> Self {
96 SshKey {
97 id: row.id,
98 title: row.title,
99 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos100 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign101 }
102 }
103}
104
105struct Identity {
106 db: D1Database,
Email verification, password reset, and Git for AI scale positioning107 env: Env,
API and MCP server, Rust identity service, registration, site redesign108}
109
110impl Identity {
Workspaces own repositories111 /// Runs a query that returns at most one user, for showing to others:
112 /// without their workspaces.
113 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning114 Ok(self
115 .db
API and MCP server, Rust identity service, registration, site redesign116 .prepare(sql)
117 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning118 .first::<Account>(None)
119 .await?
120 .map(User::from))
121 }
122
Workspaces own repositories123 /// Attaches the workspaces a user belongs to, so that any service can
124 /// authorize them without asking again.
125 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
126 let Some(mut user) = user else {
127 return Ok(None);
128 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 let memberships = self.memberships(&user.id).await?;
130 // Access to a workspace is used only within its policy; see security.rs.
131 user.workspaces = self.within_policy(&user.id, memberships).await?;
132 // Roles on single repositories, under the same policy (access.rs).
133 let grants = self.grants_of(&user.id).await?;
134 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories135 Ok(Some(user))
136 }
137
138 /// Runs a query that resolves credentials to at most one user.
139 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
140 let user = self.find_public_user(sql, param).await?;
141 self.with_workspaces(user).await
142 }
143
Email verification, password reset, and Git for AI scale positioning144 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos145 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning146 let token = crypto::random_hex(32);
147 self.db
RFC 3339 timestamps in identity and repos148 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning149 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos150 VALUES (?, ?, ?, {})",
151 sql_after(ttl)
152 ))
Email verification, password reset, and Git for AI scale positioning153 .bind(&[
154 crypto::sha256_hex(&token).into(),
155 user_id.into(),
156 kind.into(),
157 ])?
158 .run()
159 .await?;
160 Ok(token)
API and MCP server, Rust identity service, registration, site redesign161 }
162
Email verification, password reset, and Git for AI scale positioning163 /// Consumes a token of `kind`, returning its owner if it was valid.
164 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
165 let id = crypto::sha256_hex(token);
166 let owner = self
167 .db
RFC 3339 timestamps in identity and repos168 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look169 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning170 JOIN users ON users.id = email_tokens.user_id
171 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos172 AND email_tokens.expires_at > {SQL_NOW}"
173 ))
Email verification, password reset, and Git for AI scale positioning174 .bind(&[id.as_str().into(), kind.into()])?
175 .first::<TokenOwner>(None)
176 .await?;
177 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look178 // Every outstanding token of this kind dies with the one used:
179 // every reset link, and every confirmation link for the same
180 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning181 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look182 .prepare(
183 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
184 AND (?2 = 'reset' OR email_id IS ?3)",
185 )
186 .bind(&[
187 owner.id.as_str().into(),
188 kind.into(),
189 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
190 ])?
Email verification, password reset, and Git for AI scale positioning191 .run()
192 .await?;
193 }
194 Ok(owner)
195 }
196
197 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
198 let token = self
199 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
200 .await?;
201 email::send_verification(&self.env, email, &user.username, &token).await
202 }
203
204 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 look205 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
206 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
207 }
208 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning209 }
210
211 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
212 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
213 return Ok(Outcome::fail(
214 FailureCode::Invalid,
215 "This confirmation link is not valid or has expired.",
216 ));
217 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look218 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
219 return Ok(Outcome::Fail(failure));
220 }
221 // Whether the account is confirmed: whether its primary is.
222 let verified = self
223 .find_public_user(
224 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
225 &owner.id,
226 )
227 .await?
228 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning229 Ok(Outcome::Ok(User {
230 id: owner.id,
231 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look232 verified,
Workspaces own repositories233 ..User::default()
Email verification, password reset, and Git for AI scale positioning234 }))
235 }
236
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 /// Any confirmed address of an account can ask for a reset; so can the
238 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning239 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 look240 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
241 && match a.client.as_deref() {
242 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
243 None => true,
244 };
245 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 exists246 // A failure from here on happens only for a real account, so it
247 // is logged, never answered: the reply below stays the same.
248 if let Err(error) = self.send_reset(&target).await {
249 worker::console_error!("password reset for a known address failed: {error}");
250 }
251 }
252 // The same answer either way, so addresses cannot be probed.
253 Ok(true)
254 }
255
256 /// Saves a reset link for `target` and mails it, telling the account's
257 /// other addresses.
258 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
259 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look260 let token = crypto::random_hex(32);
261 self.db
262 .prepare(format!(
263 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
264 VALUES (?, ?, 'reset', {}, ?)",
265 sql_after(RESET_TTL_SECONDS)
266 ))
267 .bind(&[
268 crypto::sha256_hex(&token).into(),
269 target.user_id.as_str().into(),
270 target.email_id.as_str().into(),
271 ])?
272 .run()
Email verification, password reset, and Git for AI scale positioning273 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look274 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
275 // The primary and the backup hear of it when it went elsewhere.
276 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
277 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
278 let change = format!("A password reset was asked for through {}", target.display);
279 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
280 worker::console_error!("security notice failed: {error}");
281 }
282 }
Email verification, password reset, and Git for AI scale positioning283 }
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 exists284 Ok(())
Email verification, password reset, and Git for AI scale positioning285 }
286
287 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
288 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
289 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
290 }
291 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
292 return Ok(Outcome::fail(
293 FailureCode::Invalid,
294 "This reset link is not valid or has expired.",
295 ));
296 };
297 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look298 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning299 .bind(&[
300 crypto::hash_password(&a.password).into(),
301 owner.id.as_str().into(),
302 ])?
303 .run()
304 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look305 // Following an emailed link also proves the address it went to
306 // (unless another account confirmed it first).
307 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
308 // Anyone signed in with the old password is signed out, and nobody
309 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning310 self.db
311 .prepare("DELETE FROM sessions WHERE user_id = ?")
312 .bind(&[owner.id.as_str().into()])?
313 .run()
314 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look315 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
316 self.log_security(&owner.id, "password_changed", None, None).await;
317 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
318 let verified = self
319 .find_public_user(
320 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
321 &owner.id,
322 )
323 .await?
324 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning325 Ok(Outcome::Ok(User {
326 id: owner.id,
327 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look328 verified,
Workspaces own repositories329 ..User::default()
Email verification, password reset, and Git for AI scale positioning330 }))
331 }
332
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 /// The account a login names: a username, or any confirmed address.
334 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
335 let login = login.trim().to_lowercase();
336 let (column, value) = if login.contains('@') {
337 match self.user_with_verified_email(&login).await? {
338 Some(id) => ("id", id),
339 None => return Ok(None),
340 }
341 } else {
342 ("username", login)
343 };
344 self.db
345 .prepare(format!(
346 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
347 ))
348 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign349 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look350 .await
351 }
352
353 /// Checks a password for a login, throttled (see throttle.rs). The
354 /// refusal is one of two messages, the same for every account.
355 async fn checked_password(
356 &self,
357 login: &str,
358 password: &str,
359 client: Option<&str>,
360 ) -> Result<std::result::Result<User, &'static str>> {
361 let row = self.password_row(login).await?;
362 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
363 let (account_key, client_key) = Identity::password_keys(&subject, client);
364 if self.password_locked(&account_key, client_key.as_deref()).await? {
365 return Ok(Err(throttle::THROTTLED));
366 }
367 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
368 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
369 Some(row) => {
370 self.clear(&account_key).await?;
371 Ok(Ok(User {
372 id: row.id,
373 username: row.username,
374 verified: row.verified != 0,
375 ..User::default()
376 }))
377 }
378 None => {
379 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
380 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
381 Ok(Err("Incorrect username or password."))
382 }
383 }
384 }
385
386 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
387 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories388 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign389 }
390
391 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
392 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-agent393 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign394 let email = a.email.trim().to_lowercase();
395 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 look396 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
397 // The invite first: without one, nothing else on the form matters.
398 if self.invites_required() && invite_code.is_none() {
399 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
400 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent401 if !claimable {
API and MCP server, Rust identity service, registration, site redesign402 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent403 "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 redesign404 );
405 }
406 let well_formed_email = email
407 .split_once('@')
408 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
409 && !email.contains(char::is_whitespace);
410 if !well_formed_email {
411 return invalid("Enter a valid email address.");
412 }
413 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning414 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign415 }
416 let taken = self
417 .db
Agents as a team: lifecycle, merge queue, billing and a new shell418 // Usernames and workspaces share one namespace, so that a name
419 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look420 // An address is taken once an account has confirmed it; an
421 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell422 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look423 "SELECT username FROM users WHERE username = ?
424 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 shell425 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
426 )
427 .bind(&[
428 username.as_str().into(),
429 email.as_str().into(),
430 username.as_str().into(),
431 ])?
API and MCP server, Rust identity service, registration, site redesign432 .first::<serde_json::Value>(None)
433 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look434 // A renamed workspace's old slug stays reserved for it a while, and
435 // a deleted workspace's for good.
436 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign437 return Ok(Outcome::fail(
438 FailureCode::Conflict,
439 "That username or email is already registered.",
440 ));
441 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look442 let password_hash = crypto::hash_password(&a.password);
443 let user = match self
444 .create_account(invites::NewAccount {
445 username: &username,
446 email: &email,
447 password_hash: &password_hash,
448 verified: false,
449 invite_code,
450 client: a.client.as_deref(),
451 })
452 .await?
453 {
454 Outcome::Ok(user) => user,
455 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign456 };
Email verification, password reset, and Git for AI scale positioning457 // 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 quotas458 // An invite sent to this address confirmed it already (invites.rs).
459 if !user.verified
460 && let Err(error) = self.send_verification(&user, &email).await
461 {
Email verification, password reset, and Git for AI scale positioning462 worker::console_error!("verification email failed: {error}");
463 }
API and MCP server, Rust identity service, registration, site redesign464 self.start_session(user).await
465 }
466
467 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 look468 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
469 Ok(user) => user,
470 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign471 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look472 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign473 self.start_session(user).await
474 }
475
476 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
477 let session_token = crypto::random_hex(32);
478 self.db
RFC 3339 timestamps in identity and repos479 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look480 // Signing in is proof it is the person: see security.rs.
481 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos482 sql_after(SESSION_TTL_SECONDS)
483 ))
API and MCP server, Rust identity service, registration, site redesign484 .bind(&[
485 crypto::sha256_hex(&session_token).into(),
486 user.id.as_str().into(),
487 ])?
488 .run()
489 .await?;
490 Ok(Outcome::Ok(SignedIn {
491 user,
492 session_token,
493 }))
494 }
495
496 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
497 self.db
498 .prepare("DELETE FROM sessions WHERE id = ?")
499 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
500 .run()
501 .await?;
502 Ok(())
503 }
504
505 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
506 self.find_user(
RFC 3339 timestamps in identity and repos507 &format!(
Workspace names and icons, and a component kit for every control508 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
509 users.avatar
RFC 3339 timestamps in identity and repos510 FROM sessions JOIN users ON users.id = sessions.user_id
511 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
512 ),
API and MCP server, Rust identity service, registration, site redesign513 &crypto::sha256_hex(&a.session_token),
514 )
515 .await
516 }
517
518 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
519 // Like GitHub, a token alone identifies its user.
520 if a.secret.starts_with(TOKEN_PREFIX) {
521 self.user_for_access_token(&a.secret).await
522 } else {
523 self.user_for_password(&a.username, &a.secret).await
524 }
525 }
526
527 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
528 self.find_user(
Email verification, password reset, and Git for AI scale positioning529 "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 redesign530 JOIN users ON users.id = ssh_keys.user_id
531 WHERE fingerprint = ?",
532 &a.fingerprint,
533 )
534 .await
535 }
536
537 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories538 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning539 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign540 &a.username.to_lowercase(),
541 )
542 .await
543 }
544
Inbox: threads, reasons, subscriptions and watching545 /// `notify_by_email`: an inbox item, emailed to the person it is for,
546 /// only at a confirmed address and only while they can still read the
547 /// repository it is about. Returns whether it was sent.
548 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
549 #[derive(Deserialize)]
550 struct Address {
551 email: Option<String>,
552 }
553 let user = self
554 .find_user(
555 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
556 &a.username.to_lowercase(),
557 )
558 .await?;
559 let Some(user) = user.filter(|user| user.verified) else {
560 return Ok(false);
561 };
562 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
563 &self.env.service("REPOS")?,
564 "readable",
565 &g1t_contracts::repos::ReadableArgs {
566 ids: vec![a.repo_id.clone()],
567 viewer: Some(user.clone()),
568 },
569 )
570 .await?;
571 if readable.is_empty() {
572 return Ok(false);
573 }
574 let address = self
575 .db
576 .prepare("SELECT email FROM users WHERE id = ?")
577 .bind(&[user.id.as_str().into()])?
578 .first::<Address>(None)
579 .await?
580 .and_then(|row| row.email)
581 .filter(|email| !email.trim().is_empty());
582 let Some(address) = address else {
583 return Ok(false);
584 };
585 email::send_notification(&self.env, &address, &a).await?;
586 Ok(true)
587 }
588
What happened across an outcome, as a feed beside its graph589 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
590 #[derive(serde::Deserialize)]
591 struct Named {
592 id: String,
593 name: String,
594 }
595 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
596 let mut names = std::collections::HashMap::new();
597 if ids.is_empty() {
598 return Ok(names);
599 }
600 let marks = vec!["?"; ids.len()].join(", ");
601 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
602 for sql in [
603 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
604 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
605 ] {
606 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
607 names.insert(row.id, row.name);
608 }
609 }
610 Ok(names)
611 }
612
API and MCP server, Rust identity service, registration, site redesign613 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
614 let rows = self
615 .db
616 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
617 .bind(&[a.user.id.into()])?
618 .all()
619 .await?
620 .results::<KeyRow>()?;
621 Ok(rows.into_iter().map(SshKey::from).collect())
622 }
623
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge624 /// The account (user id) that registered each key, by fingerprint
625 /// (`SHA256:…`). At most 100; unknown keys are left out.
626 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
627 #[derive(serde::Deserialize)]
628 struct Row {
629 fingerprint: String,
630 user_id: String,
631 }
632 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
633 if fingerprints.is_empty() {
634 return Ok(std::collections::HashMap::new());
635 }
636 let marks = vec!["?"; fingerprints.len()].join(", ");
637 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
638 Ok(self
639 .db
640 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
641 .bind(&binds)?
642 .all()
643 .await?
644 .results::<Row>()?
645 .into_iter()
646 .map(|row| (row.fingerprint, row.user_id))
647 .collect())
648 }
649
API and MCP server, Rust identity service, registration, site redesign650 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
651 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
652 return Ok(Outcome::fail(
653 FailureCode::Invalid,
654 "That is not a valid OpenSSH public key.",
655 ));
656 };
657 let taken = self
658 .db
659 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
660 .bind(&[key.fingerprint.as_str().into()])?
661 .first::<serde_json::Value>(None)
662 .await?;
663 if taken.is_some() {
664 return Ok(Outcome::fail(
665 FailureCode::Conflict,
666 "That key is already registered.",
667 ));
668 }
669 let now = now_ms();
670 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
671 .into_iter()
672 .find(|candidate| !candidate.is_empty())
673 .unwrap_or_default()
674 .to_owned();
675 let row = KeyRow {
676 id: new_id("key", now),
677 title,
678 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos679 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign680 };
681 self.db
682 .prepare(
683 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
684 VALUES (?, ?, ?, ?, ?, ?)",
685 )
686 .bind(&[
687 row.id.as_str().into(),
688 a.user.id.into(),
689 row.title.as_str().into(),
690 key.public_key.into(),
691 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos692 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign693 ])?
694 .run()
695 .await?;
696 Ok(Outcome::Ok(row.into()))
697 }
698
699 /// Deletes a row the user owns from `table`.
700 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
701 self.db
702 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
703 .bind(&[a.id.into(), a.user.id.into()])?
704 .run()
705 .await?;
706 Ok(())
707 }
708}
709
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas710/// 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 member711/// the last summary's window was still open, so none waits on a later one;
712/// and deleted workspaces past their restore window are purged
713/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas714#[event(scheduled)]
715async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
716 let Ok(db) = env.d1("DB") else { return };
717 let identity = Identity { db, env };
718 if let Err(error) = identity.notify_staff_of_requests().await {
719 worker::console_error!("waitlist summary: {error}");
720 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member721 if let Err(error) = identity.purge_due_workspaces().await {
722 worker::console_error!("workspace purge: {error}");
723 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas724}
725
API and MCP server, Rust identity service, registration, site redesign726#[event(fetch)]
727async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
728 let Some(method) = rpc_method(&request) else {
729 return Response::error("Not found", 404);
730 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents731 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
732 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign733 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents734 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign735
Fast pages, required checks on the branch, self-hosted runners, honest incidents736 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette737 "register" => {
738 let outcome = identity.register(args(body)?).await?;
739 if let Outcome::Ok(signed_in) = &outcome {
740 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
741 }
742 reply(&outcome)
743 }
API and MCP server, Rust identity service, registration, site redesign744 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette745 "create_workspace" => {
746 let outcome = identity.create_workspace(args(body)?).await?;
747 if let Outcome::Ok(workspace) = &outcome {
748 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
749 }
750 reply(&outcome)
751 }
Workspaces own repositories752 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
753 "list_members" => reply(&identity.list_members(args(body)?).await?),
754 "add_member" => reply(&identity.add_member(args(body)?).await?),
755 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette756 "update_workspace" => {
757 let outcome = identity.update_workspace(args(body)?).await?;
758 if let Outcome::Ok(workspace) = &outcome {
759 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
760 }
761 reply(&outcome)
762 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains763 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
764 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
765 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'766 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look767 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
768 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
769 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette770 "set_workspace_avatar" => {
771 let outcome = identity.set_workspace_avatar(args(body)?).await?;
772 if let Outcome::Ok(workspace) = &outcome {
773 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
774 }
775 reply(&outcome)
776 }
777 "set_user_avatar" => {
778 let a: SetUserAvatarArgs = args(body)?;
779 let (username, id) = (a.user.username.clone(), a.user.id.clone());
780 let outcome = identity.set_user_avatar(a).await?;
781 if matches!(outcome, Outcome::Ok(_)) {
782 identity.announce_user(&username, Some(&id)).await;
783 }
784 reply(&outcome)
785 }
Agents as a team: lifecycle, merge queue, billing and a new shell786 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
787 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
788 "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 look789 // Signing in with GitHub; see github.rs.
790 "github_enabled" => reply(&identity.github_enabled()),
791 "github_start" => reply(&identity.github_start(args(body)?).await?),
792 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
793 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
794 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
795 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
796 "github_account" => reply(&identity.github_account(args(body)?).await?),
797 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
798 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
799 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
800 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications801 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
802 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
803 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
804 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
805 "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 step806 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API807 "device_start" => reply(&identity.device_start(args(body)?).await?),
808 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
809 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
810 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning811 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
812 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
813 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
814 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look815 // A person's email addresses; see emails.rs and security.rs.
816 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
817 "add_email" => reply(&identity.add_email(args(body)?).await?),
818 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
819 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
820 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
821 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
822 "security_log" => reply(&identity.security_log(args(body)?).await?),
823 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
824 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
825 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
826 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
827 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign828 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
829 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
830 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
831 "user_for_access_token" => {
832 let a: TokenArgs = args(body)?;
833 reply(&identity.user_for_access_token(&a.token).await?)
834 }
835 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
836 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph837 "usernames" => reply(&identity.usernames(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching838 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains839 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette840 "update_profile" => {
841 let outcome = identity.update_profile(args(body)?).await?;
842 if let Outcome::Ok(profile) = &outcome {
843 identity.announce_user(&profile.username, None).await;
844 }
845 reply(&outcome)
846 }
847 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains848 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign849 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge850 // Services only: who registered each key, for verifying commit
851 // signatures (repos' signatures.rs).
852 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign853 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
854 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
855 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
856 "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 step857 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell858 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
859 "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 API860 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
861 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
862 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign863 "remove_access_token" => reply(&identity.remove("access_tokens", args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864 // Invites and the waitlist; see invites.rs.
865 "registration" => reply(&identity.registration_mode()),
866 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
867 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
868 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
869 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
870 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
871 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
872 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
873 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
874 "request_access" => reply(&identity.request_access(args(body)?).await?),
875 // Who has access to a repository; see access.rs.
876 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
877 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
878 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
879 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
880 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
881 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
882 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
883 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
884 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'885 // Where a workspace keeps its repositories' git data (EU residency).
886 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
887 "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 look888 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
889 "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 bar890 // Teams (teams.rs).
891 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
892 "get_team" => reply(&identity.get_team(args(body)?).await?),
893 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'894 "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 bar895 "update_team" => reply(&identity.update_team(args(body)?).await?),
896 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
897 "team_members" => reply(&identity.team_members(args(body)?).await?),
898 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
899 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
900 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
901 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
902 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
903 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
904 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
905 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
906 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
907 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace908 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
909 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
910 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
911 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look912 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
913 "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 quotas914 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
916 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
917 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
918 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
919 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
920 "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 member921 // Deleted workspaces, restored or purged by staff; see deletion.rs.
922 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
923 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
924 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'925 // Workspace aliases, set by staff only; see aliases.rs.
926 "admin_aliases" => reply(&identity.admin_aliases().await?),
927 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
928 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign929 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents930 };
931 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign932}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent933
934#[cfg(test)]
935mod register_tests {
936 use super::*;
937
938 #[test]
939 fn nobody_registers_as_g1t() {
940 // What register checks the username with, whatever its case.
941 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
942 assert_eq!(claimable_namespace(username), None, "{username}");
943 }
944 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
945 }
946}

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