Skip to content

g1t/services/identity/src/lib.rs

911 lines40,823 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;
Workspace names and icons, and a component kit for every control8mod avatars;
API and MCP server, Rust identity service, registration, site redesign9mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look10mod deletion;
Device sign-in replaces registering and minting tokens over the API11mod device;
Search across all of g1t, Explore, and a command palette12mod directory;
Email verification, password reset, and Git for AI scale positioning13mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14mod emails;
15mod github;
16mod invites;
OAuth 2.1 sign-in for MCP clients and other applications17mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person18mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains19mod profiles;
20mod rename;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API21mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar23mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell25mod tokens;
Workspaces own repositories26mod workspaces;
API and MCP server, Rust identity service, registration, site redesign27
28use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos29use 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-agent30use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign31use g1t_kit::{args, now_ms, reply, rpc_method};
32use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell33use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign34use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas35use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign36
RFC 3339 timestamps in identity and repos37const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
38const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
39const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign40const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning41const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
42
43/// A user as selected from the database; `verified` arrives as 0 or 1.
44#[derive(Deserialize)]
45struct Account {
46 id: String,
47 username: String,
48 verified: u8,
Workspace names and icons, and a component kit for every control49 /// Selected only where the person is being shown to themselves.
50 #[serde(default)]
51 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning52}
53
54impl From<Account> for User {
55 fn from(row: Account) -> Self {
56 User {
57 id: row.id,
58 username: row.username,
59 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control60 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell61 ..User::default()
Email verification, password reset, and Git for AI scale positioning62 }
63 }
64}
API and MCP server, Rust identity service, registration, site redesign65
66#[derive(Deserialize)]
67struct UserRow {
68 id: String,
69 username: String,
70 password_hash: String,
Email verification, password reset, and Git for AI scale positioning71 verified: u8,
API and MCP server, Rust identity service, registration, site redesign72}
73
Email verification, password reset, and Git for AI scale positioning74/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign75#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning76struct TokenOwner {
77 id: String,
78 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look79 /// The address a link was sent to; null on links from before accounts
80 /// had several, which are for the primary.
81 #[serde(default)]
82 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning83}
84
85#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign86struct KeyRow {
87 id: String,
88 title: String,
89 fingerprint: String,
RFC 3339 timestamps in identity and repos90 created_at: String,
API and MCP server, Rust identity service, registration, site redesign91}
92
93impl From<KeyRow> for SshKey {
94 fn from(row: KeyRow) -> Self {
95 SshKey {
96 id: row.id,
97 title: row.title,
98 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos99 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign100 }
101 }
102}
103
104struct Identity {
105 db: D1Database,
Email verification, password reset, and Git for AI scale positioning106 env: Env,
API and MCP server, Rust identity service, registration, site redesign107}
108
109impl Identity {
Workspaces own repositories110 /// Runs a query that returns at most one user, for showing to others:
111 /// without their workspaces.
112 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning113 Ok(self
114 .db
API and MCP server, Rust identity service, registration, site redesign115 .prepare(sql)
116 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning117 .first::<Account>(None)
118 .await?
119 .map(User::from))
120 }
121
Workspaces own repositories122 /// Attaches the workspaces a user belongs to, so that any service can
123 /// authorize them without asking again.
124 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
125 let Some(mut user) = user else {
126 return Ok(None);
127 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look128 let memberships = self.memberships(&user.id).await?;
129 // Access to a workspace is used only within its policy; see security.rs.
130 user.workspaces = self.within_policy(&user.id, memberships).await?;
131 // Roles on single repositories, under the same policy (access.rs).
132 let grants = self.grants_of(&user.id).await?;
133 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories134 Ok(Some(user))
135 }
136
137 /// Runs a query that resolves credentials to at most one user.
138 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
139 let user = self.find_public_user(sql, param).await?;
140 self.with_workspaces(user).await
141 }
142
Email verification, password reset, and Git for AI scale positioning143 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos144 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning145 let token = crypto::random_hex(32);
146 self.db
RFC 3339 timestamps in identity and repos147 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning148 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos149 VALUES (?, ?, ?, {})",
150 sql_after(ttl)
151 ))
Email verification, password reset, and Git for AI scale positioning152 .bind(&[
153 crypto::sha256_hex(&token).into(),
154 user_id.into(),
155 kind.into(),
156 ])?
157 .run()
158 .await?;
159 Ok(token)
API and MCP server, Rust identity service, registration, site redesign160 }
161
Email verification, password reset, and Git for AI scale positioning162 /// Consumes a token of `kind`, returning its owner if it was valid.
163 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
164 let id = crypto::sha256_hex(token);
165 let owner = self
166 .db
RFC 3339 timestamps in identity and repos167 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look168 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning169 JOIN users ON users.id = email_tokens.user_id
170 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos171 AND email_tokens.expires_at > {SQL_NOW}"
172 ))
Email verification, password reset, and Git for AI scale positioning173 .bind(&[id.as_str().into(), kind.into()])?
174 .first::<TokenOwner>(None)
175 .await?;
176 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look177 // Every outstanding token of this kind dies with the one used:
178 // every reset link, and every confirmation link for the same
179 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning180 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look181 .prepare(
182 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
183 AND (?2 = 'reset' OR email_id IS ?3)",
184 )
185 .bind(&[
186 owner.id.as_str().into(),
187 kind.into(),
188 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
189 ])?
Email verification, password reset, and Git for AI scale positioning190 .run()
191 .await?;
192 }
193 Ok(owner)
194 }
195
196 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
197 let token = self
198 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
199 .await?;
200 email::send_verification(&self.env, email, &user.username, &token).await
201 }
202
203 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 look204 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
205 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
206 }
207 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning208 }
209
210 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
211 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
212 return Ok(Outcome::fail(
213 FailureCode::Invalid,
214 "This confirmation link is not valid or has expired.",
215 ));
216 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look217 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
218 return Ok(Outcome::Fail(failure));
219 }
220 // Whether the account is confirmed: whether its primary is.
221 let verified = self
222 .find_public_user(
223 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
224 &owner.id,
225 )
226 .await?
227 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning228 Ok(Outcome::Ok(User {
229 id: owner.id,
230 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look231 verified,
Workspaces own repositories232 ..User::default()
Email verification, password reset, and Git for AI scale positioning233 }))
234 }
235
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 /// Any confirmed address of an account can ask for a reset; so can the
237 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning238 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 look239 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
240 && match a.client.as_deref() {
241 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
242 None => true,
243 };
244 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 exists245 // A failure from here on happens only for a real account, so it
246 // is logged, never answered: the reply below stays the same.
247 if let Err(error) = self.send_reset(&target).await {
248 worker::console_error!("password reset for a known address failed: {error}");
249 }
250 }
251 // The same answer either way, so addresses cannot be probed.
252 Ok(true)
253 }
254
255 /// Saves a reset link for `target` and mails it, telling the account's
256 /// other addresses.
257 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
258 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look259 let token = crypto::random_hex(32);
260 self.db
261 .prepare(format!(
262 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
263 VALUES (?, ?, 'reset', {}, ?)",
264 sql_after(RESET_TTL_SECONDS)
265 ))
266 .bind(&[
267 crypto::sha256_hex(&token).into(),
268 target.user_id.as_str().into(),
269 target.email_id.as_str().into(),
270 ])?
271 .run()
Email verification, password reset, and Git for AI scale positioning272 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look273 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
274 // The primary and the backup hear of it when it went elsewhere.
275 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
276 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
277 let change = format!("A password reset was asked for through {}", target.display);
278 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
279 worker::console_error!("security notice failed: {error}");
280 }
281 }
Email verification, password reset, and Git for AI scale positioning282 }
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 exists283 Ok(())
Email verification, password reset, and Git for AI scale positioning284 }
285
286 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
287 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
288 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
289 }
290 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
291 return Ok(Outcome::fail(
292 FailureCode::Invalid,
293 "This reset link is not valid or has expired.",
294 ));
295 };
296 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look297 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning298 .bind(&[
299 crypto::hash_password(&a.password).into(),
300 owner.id.as_str().into(),
301 ])?
302 .run()
303 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 // Following an emailed link also proves the address it went to
305 // (unless another account confirmed it first).
306 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
307 // Anyone signed in with the old password is signed out, and nobody
308 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning309 self.db
310 .prepare("DELETE FROM sessions WHERE user_id = ?")
311 .bind(&[owner.id.as_str().into()])?
312 .run()
313 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look314 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
315 self.log_security(&owner.id, "password_changed", None, None).await;
316 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
317 let verified = self
318 .find_public_user(
319 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
320 &owner.id,
321 )
322 .await?
323 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning324 Ok(Outcome::Ok(User {
325 id: owner.id,
326 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look327 verified,
Workspaces own repositories328 ..User::default()
Email verification, password reset, and Git for AI scale positioning329 }))
330 }
331
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look332 /// The account a login names: a username, or any confirmed address.
333 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
334 let login = login.trim().to_lowercase();
335 let (column, value) = if login.contains('@') {
336 match self.user_with_verified_email(&login).await? {
337 Some(id) => ("id", id),
338 None => return Ok(None),
339 }
340 } else {
341 ("username", login)
342 };
343 self.db
344 .prepare(format!(
345 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
346 ))
347 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign348 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look349 .await
350 }
351
352 /// Checks a password for a login, throttled (see throttle.rs). The
353 /// refusal is one of two messages, the same for every account.
354 async fn checked_password(
355 &self,
356 login: &str,
357 password: &str,
358 client: Option<&str>,
359 ) -> Result<std::result::Result<User, &'static str>> {
360 let row = self.password_row(login).await?;
361 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
362 let (account_key, client_key) = Identity::password_keys(&subject, client);
363 if self.password_locked(&account_key, client_key.as_deref()).await? {
364 return Ok(Err(throttle::THROTTLED));
365 }
366 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
367 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
368 Some(row) => {
369 self.clear(&account_key).await?;
370 Ok(Ok(User {
371 id: row.id,
372 username: row.username,
373 verified: row.verified != 0,
374 ..User::default()
375 }))
376 }
377 None => {
378 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
379 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
380 Ok(Err("Incorrect username or password."))
381 }
382 }
383 }
384
385 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
386 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories387 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign388 }
389
390 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
391 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-agent392 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign393 let email = a.email.trim().to_lowercase();
394 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 look395 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
396 // The invite first: without one, nothing else on the form matters.
397 if self.invites_required() && invite_code.is_none() {
398 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
399 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent400 if !claimable {
API and MCP server, Rust identity service, registration, site redesign401 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent402 "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 redesign403 );
404 }
405 let well_formed_email = email
406 .split_once('@')
407 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
408 && !email.contains(char::is_whitespace);
409 if !well_formed_email {
410 return invalid("Enter a valid email address.");
411 }
412 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning413 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign414 }
415 let taken = self
416 .db
Agents as a team: lifecycle, merge queue, billing and a new shell417 // Usernames and workspaces share one namespace, so that a name
418 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look419 // An address is taken once an account has confirmed it; an
420 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell421 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look422 "SELECT username FROM users WHERE username = ?
423 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 shell424 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
425 )
426 .bind(&[
427 username.as_str().into(),
428 email.as_str().into(),
429 username.as_str().into(),
430 ])?
API and MCP server, Rust identity service, registration, site redesign431 .first::<serde_json::Value>(None)
432 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look433 // A renamed workspace's old slug stays reserved for it a while, and
434 // a deleted workspace's for good.
435 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign436 return Ok(Outcome::fail(
437 FailureCode::Conflict,
438 "That username or email is already registered.",
439 ));
440 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look441 let password_hash = crypto::hash_password(&a.password);
442 let user = match self
443 .create_account(invites::NewAccount {
444 username: &username,
445 email: &email,
446 password_hash: &password_hash,
447 verified: false,
448 invite_code,
449 client: a.client.as_deref(),
450 })
451 .await?
452 {
453 Outcome::Ok(user) => user,
454 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign455 };
Email verification, password reset, and Git for AI scale positioning456 // 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 quotas457 // An invite sent to this address confirmed it already (invites.rs).
458 if !user.verified
459 && let Err(error) = self.send_verification(&user, &email).await
460 {
Email verification, password reset, and Git for AI scale positioning461 worker::console_error!("verification email failed: {error}");
462 }
API and MCP server, Rust identity service, registration, site redesign463 self.start_session(user).await
464 }
465
466 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 look467 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
468 Ok(user) => user,
469 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign470 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look471 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign472 self.start_session(user).await
473 }
474
475 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
476 let session_token = crypto::random_hex(32);
477 self.db
RFC 3339 timestamps in identity and repos478 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look479 // Signing in is proof it is the person: see security.rs.
480 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos481 sql_after(SESSION_TTL_SECONDS)
482 ))
API and MCP server, Rust identity service, registration, site redesign483 .bind(&[
484 crypto::sha256_hex(&session_token).into(),
485 user.id.as_str().into(),
486 ])?
487 .run()
488 .await?;
489 Ok(Outcome::Ok(SignedIn {
490 user,
491 session_token,
492 }))
493 }
494
495 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
496 self.db
497 .prepare("DELETE FROM sessions WHERE id = ?")
498 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
499 .run()
500 .await?;
501 Ok(())
502 }
503
504 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
505 self.find_user(
RFC 3339 timestamps in identity and repos506 &format!(
Workspace names and icons, and a component kit for every control507 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
508 users.avatar
RFC 3339 timestamps in identity and repos509 FROM sessions JOIN users ON users.id = sessions.user_id
510 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
511 ),
API and MCP server, Rust identity service, registration, site redesign512 &crypto::sha256_hex(&a.session_token),
513 )
514 .await
515 }
516
517 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
518 // Like GitHub, a token alone identifies its user.
519 if a.secret.starts_with(TOKEN_PREFIX) {
520 self.user_for_access_token(&a.secret).await
521 } else {
522 self.user_for_password(&a.username, &a.secret).await
523 }
524 }
525
526 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
527 self.find_user(
Email verification, password reset, and Git for AI scale positioning528 "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 redesign529 JOIN users ON users.id = ssh_keys.user_id
530 WHERE fingerprint = ?",
531 &a.fingerprint,
532 )
533 .await
534 }
535
536 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories537 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning538 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign539 &a.username.to_lowercase(),
540 )
541 .await
542 }
543
Inbox: threads, reasons, subscriptions and watching544 /// `notify_by_email`: an inbox item, emailed to the person it is for,
545 /// only at a confirmed address and only while they can still read the
546 /// repository it is about. Returns whether it was sent.
547 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
548 #[derive(Deserialize)]
549 struct Address {
550 email: Option<String>,
551 }
552 let user = self
553 .find_user(
554 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
555 &a.username.to_lowercase(),
556 )
557 .await?;
558 let Some(user) = user.filter(|user| user.verified) else {
559 return Ok(false);
560 };
561 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
562 &self.env.service("REPOS")?,
563 "readable",
564 &g1t_contracts::repos::ReadableArgs {
565 ids: vec![a.repo_id.clone()],
566 viewer: Some(user.clone()),
567 },
568 )
569 .await?;
570 if readable.is_empty() {
571 return Ok(false);
572 }
573 let address = self
574 .db
575 .prepare("SELECT email FROM users WHERE id = ?")
576 .bind(&[user.id.as_str().into()])?
577 .first::<Address>(None)
578 .await?
579 .and_then(|row| row.email)
580 .filter(|email| !email.trim().is_empty());
581 let Some(address) = address else {
582 return Ok(false);
583 };
584 email::send_notification(&self.env, &address, &a).await?;
585 Ok(true)
586 }
587
What happened across an outcome, as a feed beside its graph588 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
589 #[derive(serde::Deserialize)]
590 struct Named {
591 id: String,
592 name: String,
593 }
594 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
595 let mut names = std::collections::HashMap::new();
596 if ids.is_empty() {
597 return Ok(names);
598 }
599 let marks = vec!["?"; ids.len()].join(", ");
600 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
601 for sql in [
602 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
603 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
604 ] {
605 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
606 names.insert(row.id, row.name);
607 }
608 }
609 Ok(names)
610 }
611
API and MCP server, Rust identity service, registration, site redesign612 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
613 let rows = self
614 .db
615 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
616 .bind(&[a.user.id.into()])?
617 .all()
618 .await?
619 .results::<KeyRow>()?;
620 Ok(rows.into_iter().map(SshKey::from).collect())
621 }
622
623 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
624 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
625 return Ok(Outcome::fail(
626 FailureCode::Invalid,
627 "That is not a valid OpenSSH public key.",
628 ));
629 };
630 let taken = self
631 .db
632 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
633 .bind(&[key.fingerprint.as_str().into()])?
634 .first::<serde_json::Value>(None)
635 .await?;
636 if taken.is_some() {
637 return Ok(Outcome::fail(
638 FailureCode::Conflict,
639 "That key is already registered.",
640 ));
641 }
642 let now = now_ms();
643 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
644 .into_iter()
645 .find(|candidate| !candidate.is_empty())
646 .unwrap_or_default()
647 .to_owned();
648 let row = KeyRow {
649 id: new_id("key", now),
650 title,
651 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos652 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign653 };
654 self.db
655 .prepare(
656 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
657 VALUES (?, ?, ?, ?, ?, ?)",
658 )
659 .bind(&[
660 row.id.as_str().into(),
661 a.user.id.into(),
662 row.title.as_str().into(),
663 key.public_key.into(),
664 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos665 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign666 ])?
667 .run()
668 .await?;
669 Ok(Outcome::Ok(row.into()))
670 }
671
672 /// Deletes a row the user owns from `table`.
673 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
674 self.db
675 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
676 .bind(&[a.id.into(), a.user.id.into()])?
677 .run()
678 .await?;
679 Ok(())
680 }
681}
682
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas683/// 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 member684/// the last summary's window was still open, so none waits on a later one;
685/// and deleted workspaces past their restore window are purged
686/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas687#[event(scheduled)]
688async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
689 let Ok(db) = env.d1("DB") else { return };
690 let identity = Identity { db, env };
691 if let Err(error) = identity.notify_staff_of_requests().await {
692 worker::console_error!("waitlist summary: {error}");
693 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member694 if let Err(error) = identity.purge_due_workspaces().await {
695 worker::console_error!("workspace purge: {error}");
696 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas697}
698
API and MCP server, Rust identity service, registration, site redesign699#[event(fetch)]
700async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
701 let Some(method) = rpc_method(&request) else {
702 return Response::error("Not found", 404);
703 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents704 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
705 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign706 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents707 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign708
Fast pages, required checks on the branch, self-hosted runners, honest incidents709 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette710 "register" => {
711 let outcome = identity.register(args(body)?).await?;
712 if let Outcome::Ok(signed_in) = &outcome {
713 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
714 }
715 reply(&outcome)
716 }
API and MCP server, Rust identity service, registration, site redesign717 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette718 "create_workspace" => {
719 let outcome = identity.create_workspace(args(body)?).await?;
720 if let Outcome::Ok(workspace) = &outcome {
721 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
722 }
723 reply(&outcome)
724 }
Workspaces own repositories725 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
726 "list_members" => reply(&identity.list_members(args(body)?).await?),
727 "add_member" => reply(&identity.add_member(args(body)?).await?),
728 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette729 "update_workspace" => {
730 let outcome = identity.update_workspace(args(body)?).await?;
731 if let Outcome::Ok(workspace) = &outcome {
732 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
733 }
734 reply(&outcome)
735 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains736 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
737 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
738 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look739 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
740 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
741 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette742 "set_workspace_avatar" => {
743 let outcome = identity.set_workspace_avatar(args(body)?).await?;
744 if let Outcome::Ok(workspace) = &outcome {
745 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
746 }
747 reply(&outcome)
748 }
749 "set_user_avatar" => {
750 let a: SetUserAvatarArgs = args(body)?;
751 let (username, id) = (a.user.username.clone(), a.user.id.clone());
752 let outcome = identity.set_user_avatar(a).await?;
753 if matches!(outcome, Outcome::Ok(_)) {
754 identity.announce_user(&username, Some(&id)).await;
755 }
756 reply(&outcome)
757 }
Agents as a team: lifecycle, merge queue, billing and a new shell758 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
759 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
760 "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 look761 // Signing in with GitHub; see github.rs.
762 "github_enabled" => reply(&identity.github_enabled()),
763 "github_start" => reply(&identity.github_start(args(body)?).await?),
764 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
765 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
766 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
767 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
768 "github_account" => reply(&identity.github_account(args(body)?).await?),
769 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
770 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
771 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
772 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications773 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
774 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
775 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
776 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
777 "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 step778 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API779 "device_start" => reply(&identity.device_start(args(body)?).await?),
780 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
781 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
782 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning783 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
784 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
785 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
786 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 // A person's email addresses; see emails.rs and security.rs.
788 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
789 "add_email" => reply(&identity.add_email(args(body)?).await?),
790 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
791 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
792 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
793 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
794 "security_log" => reply(&identity.security_log(args(body)?).await?),
795 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
796 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
797 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
798 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
799 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign800 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
801 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
802 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
803 "user_for_access_token" => {
804 let a: TokenArgs = args(body)?;
805 reply(&identity.user_for_access_token(&a.token).await?)
806 }
807 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
808 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph809 "usernames" => reply(&identity.usernames(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching810 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains811 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette812 "update_profile" => {
813 let outcome = identity.update_profile(args(body)?).await?;
814 if let Outcome::Ok(profile) = &outcome {
815 identity.announce_user(&profile.username, None).await;
816 }
817 reply(&outcome)
818 }
819 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains820 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign821 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
822 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
823 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
824 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
825 "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 step826 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell827 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
828 "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 API829 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
830 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
831 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign832 "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 look833 // Invites and the waitlist; see invites.rs.
834 "registration" => reply(&identity.registration_mode()),
835 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
836 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
837 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
838 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
839 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
840 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
841 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
842 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
843 "request_access" => reply(&identity.request_access(args(body)?).await?),
844 // Who has access to a repository; see access.rs.
845 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
846 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
847 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
848 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
849 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
850 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
851 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
852 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
853 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'854 // Where a workspace keeps its repositories' git data (EU residency).
855 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
856 "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 look857 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
858 "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 bar859 // Teams (teams.rs).
860 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
861 "get_team" => reply(&identity.get_team(args(body)?).await?),
862 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'863 "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 bar864 "update_team" => reply(&identity.update_team(args(body)?).await?),
865 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
866 "team_members" => reply(&identity.team_members(args(body)?).await?),
867 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
868 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
869 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
870 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
871 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
872 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
873 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
874 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
875 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
876 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace877 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
878 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
879 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
880 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look881 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
882 "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 quotas883 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
885 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
886 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
887 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
888 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
889 "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 member890 // Deleted workspaces, restored or purged by staff; see deletion.rs.
891 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
892 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
893 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign894 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents895 };
896 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign897}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent898
899#[cfg(test)]
900mod register_tests {
901 use super::*;
902
903 #[test]
904 fn nobody_registers_as_g1t() {
905 // What register checks the username with, whatever its case.
906 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
907 assert_eq!(claimable_namespace(username), None, "{username}");
908 }
909 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
910 }
911}

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