Skip to content
938 linesCodeBlameRaw

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

API and MCP server, Rust identity service, registration, site redesign1//! The identity service: accounts, sessions, SSH keys and access tokens.
2//!
3//! Reached only through service bindings; see `g1t_contracts::identity` for
4//! the methods and their arguments.
5
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6mod access;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7mod admin;
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;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains18mod profiles;
19mod rename;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API20mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar22mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell24mod tokens;
Workspaces own repositories25mod workspaces;
API and MCP server, Rust identity service, registration, site redesign26
27use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos28use 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-agent29use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign30use g1t_kit::{args, now_ms, reply, rpc_method};
31use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell32use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign33use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas34use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign35
RFC 3339 timestamps in identity and repos36const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
37const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
38const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign39const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning40const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
41
42/// A user as selected from the database; `verified` arrives as 0 or 1.
43#[derive(Deserialize)]
44struct Account {
45 id: String,
46 username: String,
47 verified: u8,
Workspace names and icons, and a component kit for every control48 /// Selected only where the person is being shown to themselves.
49 #[serde(default)]
50 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning51}
52
53impl From<Account> for User {
54 fn from(row: Account) -> Self {
55 User {
56 id: row.id,
57 username: row.username,
58 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control59 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell60 ..User::default()
Email verification, password reset, and Git for AI scale positioning61 }
62 }
63}
API and MCP server, Rust identity service, registration, site redesign64
65#[derive(Deserialize)]
66struct UserRow {
67 id: String,
68 username: String,
69 password_hash: String,
Email verification, password reset, and Git for AI scale positioning70 verified: u8,
API and MCP server, Rust identity service, registration, site redesign71}
72
Email verification, password reset, and Git for AI scale positioning73/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign74#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning75struct TokenOwner {
76 id: String,
77 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78 /// The address a link was sent to; null on links from before accounts
79 /// had several, which are for the primary.
80 #[serde(default)]
81 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning82}
83
84#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign85struct KeyRow {
86 id: String,
87 title: String,
88 fingerprint: String,
RFC 3339 timestamps in identity and repos89 created_at: String,
API and MCP server, Rust identity service, registration, site redesign90}
91
92impl From<KeyRow> for SshKey {
93 fn from(row: KeyRow) -> Self {
94 SshKey {
95 id: row.id,
96 title: row.title,
97 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos98 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign99 }
100 }
101}
102
103struct Identity {
104 db: D1Database,
Email verification, password reset, and Git for AI scale positioning105 env: Env,
API and MCP server, Rust identity service, registration, site redesign106}
107
108impl Identity {
Workspaces own repositories109 /// Runs a query that returns at most one user, for showing to others:
110 /// without their workspaces.
111 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning112 Ok(self
113 .db
API and MCP server, Rust identity service, registration, site redesign114 .prepare(sql)
115 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning116 .first::<Account>(None)
117 .await?
118 .map(User::from))
119 }
120
Workspaces own repositories121 /// Attaches the workspaces a user belongs to, so that any service can
122 /// authorize them without asking again.
123 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
124 let Some(mut user) = user else {
125 return Ok(None);
126 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look127 let memberships = self.memberships(&user.id).await?;
128 // Access to a workspace is used only within its policy; see security.rs.
129 user.workspaces = self.within_policy(&user.id, memberships).await?;
130 // Roles on single repositories, under the same policy (access.rs).
131 let grants = self.grants_of(&user.id).await?;
132 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories133 Ok(Some(user))
134 }
135
136 /// Runs a query that resolves credentials to at most one user.
137 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
138 let user = self.find_public_user(sql, param).await?;
139 self.with_workspaces(user).await
140 }
141
Email verification, password reset, and Git for AI scale positioning142 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos143 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning144 let token = crypto::random_hex(32);
145 self.db
RFC 3339 timestamps in identity and repos146 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning147 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos148 VALUES (?, ?, ?, {})",
149 sql_after(ttl)
150 ))
Email verification, password reset, and Git for AI scale positioning151 .bind(&[
152 crypto::sha256_hex(&token).into(),
153 user_id.into(),
154 kind.into(),
155 ])?
156 .run()
157 .await?;
158 Ok(token)
API and MCP server, Rust identity service, registration, site redesign159 }
160
Email verification, password reset, and Git for AI scale positioning161 /// Consumes a token of `kind`, returning its owner if it was valid.
162 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
163 let id = crypto::sha256_hex(token);
164 let owner = self
165 .db
RFC 3339 timestamps in identity and repos166 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look167 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning168 JOIN users ON users.id = email_tokens.user_id
169 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos170 AND email_tokens.expires_at > {SQL_NOW}"
171 ))
Email verification, password reset, and Git for AI scale positioning172 .bind(&[id.as_str().into(), kind.into()])?
173 .first::<TokenOwner>(None)
174 .await?;
175 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look176 // Every outstanding token of this kind dies with the one used:
177 // every reset link, and every confirmation link for the same
178 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning179 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look180 .prepare(
181 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
182 AND (?2 = 'reset' OR email_id IS ?3)",
183 )
184 .bind(&[
185 owner.id.as_str().into(),
186 kind.into(),
187 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
188 ])?
Email verification, password reset, and Git for AI scale positioning189 .run()
190 .await?;
191 }
192 Ok(owner)
193 }
194
195 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
196 let token = self
197 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
198 .await?;
199 email::send_verification(&self.env, email, &user.username, &token).await
200 }
201
202 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 look203 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
204 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
205 }
206 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning207 }
208
209 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
210 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
211 return Ok(Outcome::fail(
212 FailureCode::Invalid,
213 "This confirmation link is not valid or has expired.",
214 ));
215 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look216 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
217 return Ok(Outcome::Fail(failure));
218 }
219 // Whether the account is confirmed: whether its primary is.
220 let verified = self
221 .find_public_user(
222 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
223 &owner.id,
224 )
225 .await?
226 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning227 Ok(Outcome::Ok(User {
228 id: owner.id,
229 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look230 verified,
Workspaces own repositories231 ..User::default()
Email verification, password reset, and Git for AI scale positioning232 }))
233 }
234
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235 /// Any confirmed address of an account can ask for a reset; so can the
236 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning237 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 look238 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
239 && match a.client.as_deref() {
240 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
241 None => true,
242 };
243 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 exists244 // A failure from here on happens only for a real account, so it
245 // is logged, never answered: the reply below stays the same.
246 if let Err(error) = self.send_reset(&target).await {
247 worker::console_error!("password reset for a known address failed: {error}");
248 }
249 }
250 // The same answer either way, so addresses cannot be probed.
251 Ok(true)
252 }
253
254 /// Saves a reset link for `target` and mails it, telling the account's
255 /// other addresses.
256 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
257 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look258 let token = crypto::random_hex(32);
259 self.db
260 .prepare(format!(
261 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
262 VALUES (?, ?, 'reset', {}, ?)",
263 sql_after(RESET_TTL_SECONDS)
264 ))
265 .bind(&[
266 crypto::sha256_hex(&token).into(),
267 target.user_id.as_str().into(),
268 target.email_id.as_str().into(),
269 ])?
270 .run()
Email verification, password reset, and Git for AI scale positioning271 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look272 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
273 // The primary and the backup hear of it when it went elsewhere.
274 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
275 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
276 let change = format!("A password reset was asked for through {}", target.display);
277 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
278 worker::console_error!("security notice failed: {error}");
279 }
280 }
Email verification, password reset, and Git for AI scale positioning281 }
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 exists282 Ok(())
Email verification, password reset, and Git for AI scale positioning283 }
284
285 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
286 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
287 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
288 }
289 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
290 return Ok(Outcome::fail(
291 FailureCode::Invalid,
292 "This reset link is not valid or has expired.",
293 ));
294 };
295 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look296 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning297 .bind(&[
298 crypto::hash_password(&a.password).into(),
299 owner.id.as_str().into(),
300 ])?
301 .run()
302 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look303 // Following an emailed link also proves the address it went to
304 // (unless another account confirmed it first).
305 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
306 // Anyone signed in with the old password is signed out, and nobody
307 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning308 self.db
309 .prepare("DELETE FROM sessions WHERE user_id = ?")
310 .bind(&[owner.id.as_str().into()])?
311 .run()
312 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look313 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
314 self.log_security(&owner.id, "password_changed", None, None).await;
315 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
316 let verified = self
317 .find_public_user(
318 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
319 &owner.id,
320 )
321 .await?
322 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning323 Ok(Outcome::Ok(User {
324 id: owner.id,
325 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 verified,
Workspaces own repositories327 ..User::default()
Email verification, password reset, and Git for AI scale positioning328 }))
329 }
330
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look331 /// The account a login names: a username, or any confirmed address.
332 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
333 let login = login.trim().to_lowercase();
334 let (column, value) = if login.contains('@') {
335 match self.user_with_verified_email(&login).await? {
336 Some(id) => ("id", id),
337 None => return Ok(None),
338 }
339 } else {
340 ("username", login)
341 };
342 self.db
343 .prepare(format!(
344 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
345 ))
346 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign347 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look348 .await
349 }
350
351 /// Checks a password for a login, throttled (see throttle.rs). The
352 /// refusal is one of two messages, the same for every account.
353 async fn checked_password(
354 &self,
355 login: &str,
356 password: &str,
357 client: Option<&str>,
358 ) -> Result<std::result::Result<User, &'static str>> {
359 let row = self.password_row(login).await?;
360 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
361 let (account_key, client_key) = Identity::password_keys(&subject, client);
362 if self.password_locked(&account_key, client_key.as_deref()).await? {
363 return Ok(Err(throttle::THROTTLED));
364 }
365 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
366 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
367 Some(row) => {
368 self.clear(&account_key).await?;
369 Ok(Ok(User {
370 id: row.id,
371 username: row.username,
372 verified: row.verified != 0,
373 ..User::default()
374 }))
375 }
376 None => {
377 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
378 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
379 Ok(Err("Incorrect username or password."))
380 }
381 }
382 }
383
384 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
385 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories386 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign387 }
388
389 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
390 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-agent391 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign392 let email = a.email.trim().to_lowercase();
393 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 look394 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
395 // The invite first: without one, nothing else on the form matters.
396 if self.invites_required() && invite_code.is_none() {
397 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
398 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent399 if !claimable {
API and MCP server, Rust identity service, registration, site redesign400 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent401 "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 redesign402 );
403 }
404 let well_formed_email = email
405 .split_once('@')
406 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
407 && !email.contains(char::is_whitespace);
408 if !well_formed_email {
409 return invalid("Enter a valid email address.");
410 }
411 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning412 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign413 }
414 let taken = self
415 .db
Agents as a team: lifecycle, merge queue, billing and a new shell416 // Usernames and workspaces share one namespace, so that a name
417 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look418 // An address is taken once an account has confirmed it; an
419 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell420 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look421 "SELECT username FROM users WHERE username = ?
422 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 shell423 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
424 )
425 .bind(&[
426 username.as_str().into(),
427 email.as_str().into(),
428 username.as_str().into(),
429 ])?
API and MCP server, Rust identity service, registration, site redesign430 .first::<serde_json::Value>(None)
431 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look432 // A renamed workspace's old slug stays reserved for it a while, and
433 // a deleted workspace's for good.
434 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign435 return Ok(Outcome::fail(
436 FailureCode::Conflict,
437 "That username or email is already registered.",
438 ));
439 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look440 let password_hash = crypto::hash_password(&a.password);
441 let user = match self
442 .create_account(invites::NewAccount {
443 username: &username,
444 email: &email,
445 password_hash: &password_hash,
446 verified: false,
447 invite_code,
448 client: a.client.as_deref(),
449 })
450 .await?
451 {
452 Outcome::Ok(user) => user,
453 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign454 };
Email verification, password reset, and Git for AI scale positioning455 // 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 quotas456 // An invite sent to this address confirmed it already (invites.rs).
457 if !user.verified
458 && let Err(error) = self.send_verification(&user, &email).await
459 {
Email verification, password reset, and Git for AI scale positioning460 worker::console_error!("verification email failed: {error}");
461 }
API and MCP server, Rust identity service, registration, site redesign462 self.start_session(user).await
463 }
464
465 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 look466 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
467 Ok(user) => user,
468 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign469 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look470 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign471 self.start_session(user).await
472 }
473
474 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
475 let session_token = crypto::random_hex(32);
476 self.db
RFC 3339 timestamps in identity and repos477 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look478 // Signing in is proof it is the person: see security.rs.
479 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos480 sql_after(SESSION_TTL_SECONDS)
481 ))
API and MCP server, Rust identity service, registration, site redesign482 .bind(&[
483 crypto::sha256_hex(&session_token).into(),
484 user.id.as_str().into(),
485 ])?
486 .run()
487 .await?;
488 Ok(Outcome::Ok(SignedIn {
489 user,
490 session_token,
491 }))
492 }
493
494 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
495 self.db
496 .prepare("DELETE FROM sessions WHERE id = ?")
497 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
498 .run()
499 .await?;
500 Ok(())
501 }
502
503 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
504 self.find_user(
RFC 3339 timestamps in identity and repos505 &format!(
Workspace names and icons, and a component kit for every control506 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
507 users.avatar
RFC 3339 timestamps in identity and repos508 FROM sessions JOIN users ON users.id = sessions.user_id
509 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
510 ),
API and MCP server, Rust identity service, registration, site redesign511 &crypto::sha256_hex(&a.session_token),
512 )
513 .await
514 }
515
516 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
517 // Like GitHub, a token alone identifies its user.
518 if a.secret.starts_with(TOKEN_PREFIX) {
519 self.user_for_access_token(&a.secret).await
520 } else {
521 self.user_for_password(&a.username, &a.secret).await
522 }
523 }
524
525 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
526 self.find_user(
Email verification, password reset, and Git for AI scale positioning527 "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 redesign528 JOIN users ON users.id = ssh_keys.user_id
529 WHERE fingerprint = ?",
530 &a.fingerprint,
531 )
532 .await
533 }
534
535 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories536 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning537 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign538 &a.username.to_lowercase(),
539 )
540 .await
541 }
542
Inbox: threads, reasons, subscriptions and watching543 /// `notify_by_email`: an inbox item, emailed to the person it is for,
544 /// only at a confirmed address and only while they can still read the
545 /// repository it is about. Returns whether it was sent.
546 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
547 #[derive(Deserialize)]
548 struct Address {
549 email: Option<String>,
550 }
551 let user = self
552 .find_user(
553 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
554 &a.username.to_lowercase(),
555 )
556 .await?;
557 let Some(user) = user.filter(|user| user.verified) else {
558 return Ok(false);
559 };
560 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
561 &self.env.service("REPOS")?,
562 "readable",
563 &g1t_contracts::repos::ReadableArgs {
564 ids: vec![a.repo_id.clone()],
565 viewer: Some(user.clone()),
566 },
567 )
568 .await?;
569 if readable.is_empty() {
570 return Ok(false);
571 }
572 let address = self
573 .db
574 .prepare("SELECT email FROM users WHERE id = ?")
575 .bind(&[user.id.as_str().into()])?
576 .first::<Address>(None)
577 .await?
578 .and_then(|row| row.email)
579 .filter(|email| !email.trim().is_empty());
580 let Some(address) = address else {
581 return Ok(false);
582 };
583 email::send_notification(&self.env, &address, &a).await?;
584 Ok(true)
585 }
586
What happened across an outcome, as a feed beside its graph587 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
588 #[derive(serde::Deserialize)]
589 struct Named {
590 id: String,
591 name: String,
592 }
593 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
594 let mut names = std::collections::HashMap::new();
595 if ids.is_empty() {
596 return Ok(names);
597 }
598 let marks = vec!["?"; ids.len()].join(", ");
599 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
600 for sql in [
601 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
602 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
603 ] {
604 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
605 names.insert(row.id, row.name);
606 }
607 }
608 Ok(names)
609 }
610
API and MCP server, Rust identity service, registration, site redesign611 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
612 let rows = self
613 .db
614 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
615 .bind(&[a.user.id.into()])?
616 .all()
617 .await?
618 .results::<KeyRow>()?;
619 Ok(rows.into_iter().map(SshKey::from).collect())
620 }
621
Rulesets on push: refused with the ruleset and rule named, commits read622 /// The account (user id) that registered each key, by fingerprint
623 /// (`SHA256:…`). At most 100; unknown keys are left out.
624 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
625 #[derive(serde::Deserialize)]
626 struct Row {
627 fingerprint: String,
628 user_id: String,
629 }
630 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
631 if fingerprints.is_empty() {
632 return Ok(std::collections::HashMap::new());
633 }
634 let marks = vec!["?"; fingerprints.len()].join(", ");
635 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
636 Ok(self
637 .db
638 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
639 .bind(&binds)?
640 .all()
641 .await?
642 .results::<Row>()?
643 .into_iter()
644 .map(|row| (row.fingerprint, row.user_id))
645 .collect())
646 }
647
API and MCP server, Rust identity service, registration, site redesign648 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
649 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
650 return Ok(Outcome::fail(
651 FailureCode::Invalid,
652 "That is not a valid OpenSSH public key.",
653 ));
654 };
655 let taken = self
656 .db
657 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
658 .bind(&[key.fingerprint.as_str().into()])?
659 .first::<serde_json::Value>(None)
660 .await?;
661 if taken.is_some() {
662 return Ok(Outcome::fail(
663 FailureCode::Conflict,
664 "That key is already registered.",
665 ));
666 }
667 let now = now_ms();
668 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
669 .into_iter()
670 .find(|candidate| !candidate.is_empty())
671 .unwrap_or_default()
672 .to_owned();
673 let row = KeyRow {
674 id: new_id("key", now),
675 title,
676 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos677 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign678 };
679 self.db
680 .prepare(
681 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
682 VALUES (?, ?, ?, ?, ?, ?)",
683 )
684 .bind(&[
685 row.id.as_str().into(),
686 a.user.id.into(),
687 row.title.as_str().into(),
688 key.public_key.into(),
689 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos690 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign691 ])?
692 .run()
693 .await?;
694 Ok(Outcome::Ok(row.into()))
695 }
696
697 /// Deletes a row the user owns from `table`.
698 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
699 self.db
700 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
701 .bind(&[a.id.into(), a.user.id.into()])?
702 .run()
703 .await?;
704 Ok(())
705 }
706}
707
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas708/// 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 member709/// the last summary's window was still open, so none waits on a later one;
710/// and deleted workspaces past their restore window are purged
711/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas712#[event(scheduled)]
713async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
714 let Ok(db) = env.d1("DB") else { return };
715 let identity = Identity { db, env };
716 if let Err(error) = identity.notify_staff_of_requests().await {
717 worker::console_error!("waitlist summary: {error}");
718 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member719 if let Err(error) = identity.purge_due_workspaces().await {
720 worker::console_error!("workspace purge: {error}");
721 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas722}
723
API and MCP server, Rust identity service, registration, site redesign724#[event(fetch)]
725async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
726 let Some(method) = rpc_method(&request) else {
727 return Response::error("Not found", 404);
728 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents729 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
730 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign731 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents732 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign733
Fast pages, required checks on the branch, self-hosted runners, honest incidents734 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette735 "register" => {
736 let outcome = identity.register(args(body)?).await?;
737 if let Outcome::Ok(signed_in) = &outcome {
738 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
739 }
740 reply(&outcome)
741 }
API and MCP server, Rust identity service, registration, site redesign742 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette743 "create_workspace" => {
744 let outcome = identity.create_workspace(args(body)?).await?;
745 if let Outcome::Ok(workspace) = &outcome {
746 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
747 }
748 reply(&outcome)
749 }
Workspaces own repositories750 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
751 "list_members" => reply(&identity.list_members(args(body)?).await?),
752 "add_member" => reply(&identity.add_member(args(body)?).await?),
753 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette754 "update_workspace" => {
755 let outcome = identity.update_workspace(args(body)?).await?;
756 if let Outcome::Ok(workspace) = &outcome {
757 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
758 }
759 reply(&outcome)
760 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains761 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
762 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
763 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look764 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
765 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
766 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette767 "set_workspace_avatar" => {
768 let outcome = identity.set_workspace_avatar(args(body)?).await?;
769 if let Outcome::Ok(workspace) = &outcome {
770 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
771 }
772 reply(&outcome)
773 }
774 "set_user_avatar" => {
775 let a: SetUserAvatarArgs = args(body)?;
776 let (username, id) = (a.user.username.clone(), a.user.id.clone());
777 let outcome = identity.set_user_avatar(a).await?;
778 if matches!(outcome, Outcome::Ok(_)) {
779 identity.announce_user(&username, Some(&id)).await;
780 }
781 reply(&outcome)
782 }
Agents as a team: lifecycle, merge queue, billing and a new shell783 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
784 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
785 "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 look786 // Signing in with GitHub; see github.rs.
787 "github_enabled" => reply(&identity.github_enabled()),
788 "github_start" => reply(&identity.github_start(args(body)?).await?),
789 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
790 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
791 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
792 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
793 "github_account" => reply(&identity.github_account(args(body)?).await?),
794 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
795 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
796 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
797 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications798 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
799 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
800 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
801 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
802 "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 step803 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API804 "device_start" => reply(&identity.device_start(args(body)?).await?),
805 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
806 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
807 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning808 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
809 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
810 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
811 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look812 // A person's email addresses; see emails.rs and security.rs.
813 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
814 "add_email" => reply(&identity.add_email(args(body)?).await?),
815 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
816 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
817 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
818 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
819 "security_log" => reply(&identity.security_log(args(body)?).await?),
820 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
821 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
822 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
823 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
824 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign825 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
826 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
827 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
828 "user_for_access_token" => {
829 let a: TokenArgs = args(body)?;
830 reply(&identity.user_for_access_token(&a.token).await?)
831 }
832 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
833 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph834 "usernames" => reply(&identity.usernames(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching835 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains836 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette837 "update_profile" => {
838 let outcome = identity.update_profile(args(body)?).await?;
839 if let Outcome::Ok(profile) = &outcome {
840 identity.announce_user(&profile.username, None).await;
841 }
842 reply(&outcome)
843 }
844 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains845 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign846 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Rulesets on push: refused with the ruleset and rule named, commits read847 // Services only: who registered each key, for verifying commit
848 // signatures (repos' signatures.rs).
849 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign850 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
851 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
852 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
853 "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 step854 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell855 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
856 "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 API857 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
858 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
859 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign860 "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 look861 // Invites and the waitlist; see invites.rs.
862 "registration" => reply(&identity.registration_mode()),
863 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
864 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
865 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
866 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
867 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
868 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
869 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
870 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
871 "request_access" => reply(&identity.request_access(args(body)?).await?),
872 // Who has access to a repository; see access.rs.
873 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
874 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
875 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
876 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
877 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
878 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
879 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
880 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
881 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'882 // Where a workspace keeps its repositories' git data (EU residency).
883 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
884 "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 look885 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
886 "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 bar887 // Teams (teams.rs).
888 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
889 "get_team" => reply(&identity.get_team(args(body)?).await?),
890 "create_team" => reply(&identity.create_team(args(body)?).await?),
891 "update_team" => reply(&identity.update_team(args(body)?).await?),
892 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
893 "team_members" => reply(&identity.team_members(args(body)?).await?),
894 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
895 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
896 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
897 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
898 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
899 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
900 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
901 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
902 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
903 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace904 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
905 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
906 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
907 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look908 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
909 "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 quotas910 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look911 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
912 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
913 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
914 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
915 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
916 "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 member917 // Deleted workspaces, restored or purged by staff; see deletion.rs.
918 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
919 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
920 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign921 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents922 };
923 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign924}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent925
926#[cfg(test)]
927mod register_tests {
928 use super::*;
929
930 #[test]
931 fn nobody_registers_as_g1t() {
932 // What register checks the username with, whatever its case.
933 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
934 assert_eq!(claimable_namespace(username), None, "{username}");
935 }
936 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
937 }
938}

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