Skip to content
989 linesCodeBlameRaw

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

API and MCP server, Rust identity service, registration, site redesign1//! The identity service: accounts, sessions, SSH keys and access tokens.
2//!
3//! Reached only through service bindings; see `g1t_contracts::identity` for
4//! the methods and their arguments.
5
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6mod access;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'8mod aliases;
Workspace names and icons, and a component kit for every control9mod avatars;
API and MCP server, Rust identity service, registration, site redesign10mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11mod deletion;
Device sign-in replaces registering and minting tokens over the API12mod device;
Search across all of g1t, Explore, and a command palette13mod directory;
Email verification, password reset, and Git for AI scale positioning14mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15mod emails;
16mod github;
17mod invites;
OAuth 2.1 sign-in for MCP clients and other applications18mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person19mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains20mod profiles;
21mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'22mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API23mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar25mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity27mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell28mod tokens;
Workspaces own repositories29mod workspaces;
API and MCP server, Rust identity service, registration, site redesign30
31use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos32use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent33use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign34use g1t_kit::{args, now_ms, reply, rpc_method};
35use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell36use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign37use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas38use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign39
RFC 3339 timestamps in identity and repos40const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
41const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
42const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign43const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning44const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
45
46/// A user as selected from the database; `verified` arrives as 0 or 1.
47#[derive(Deserialize)]
48struct Account {
49 id: String,
50 username: String,
51 verified: u8,
Workspace names and icons, and a component kit for every control52 /// Selected only where the person is being shown to themselves.
53 #[serde(default)]
54 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning55}
56
57impl From<Account> for User {
58 fn from(row: Account) -> Self {
59 User {
60 id: row.id,
61 username: row.username,
62 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control63 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell64 ..User::default()
Email verification, password reset, and Git for AI scale positioning65 }
66 }
67}
API and MCP server, Rust identity service, registration, site redesign68
69#[derive(Deserialize)]
70struct UserRow {
71 id: String,
72 username: String,
73 password_hash: String,
Email verification, password reset, and Git for AI scale positioning74 verified: u8,
API and MCP server, Rust identity service, registration, site redesign75}
76
Email verification, password reset, and Git for AI scale positioning77/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign78#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning79struct TokenOwner {
80 id: String,
81 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 /// The address a link was sent to; null on links from before accounts
83 /// had several, which are for the primary.
84 #[serde(default)]
85 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning86}
87
88#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign89struct KeyRow {
90 id: String,
91 title: String,
92 fingerprint: String,
RFC 3339 timestamps in identity and repos93 created_at: String,
API and MCP server, Rust identity service, registration, site redesign94}
95
96impl From<KeyRow> for SshKey {
97 fn from(row: KeyRow) -> Self {
98 SshKey {
99 id: row.id,
100 title: row.title,
101 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos102 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign103 }
104 }
105}
106
107struct Identity {
108 db: D1Database,
Email verification, password reset, and Git for AI scale positioning109 env: Env,
API and MCP server, Rust identity service, registration, site redesign110}
111
112impl Identity {
Workspaces own repositories113 /// Runs a query that returns at most one user, for showing to others:
114 /// without their workspaces.
115 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning116 Ok(self
117 .db
API and MCP server, Rust identity service, registration, site redesign118 .prepare(sql)
119 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning120 .first::<Account>(None)
121 .await?
122 .map(User::from))
123 }
124
Workspaces own repositories125 /// Attaches the workspaces a user belongs to, so that any service can
126 /// authorize them without asking again.
127 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
128 let Some(mut user) = user else {
129 return Ok(None);
130 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look131 let memberships = self.memberships(&user.id).await?;
132 // Access to a workspace is used only within its policy; see security.rs.
133 user.workspaces = self.within_policy(&user.id, memberships).await?;
134 // Roles on single repositories, under the same policy (access.rs).
135 let grants = self.grants_of(&user.id).await?;
136 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories137 Ok(Some(user))
138 }
139
140 /// Runs a query that resolves credentials to at most one user.
141 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
142 let user = self.find_public_user(sql, param).await?;
143 self.with_workspaces(user).await
144 }
145
Email verification, password reset, and Git for AI scale positioning146 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos147 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning148 let token = crypto::random_hex(32);
149 self.db
RFC 3339 timestamps in identity and repos150 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning151 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos152 VALUES (?, ?, ?, {})",
153 sql_after(ttl)
154 ))
Email verification, password reset, and Git for AI scale positioning155 .bind(&[
156 crypto::sha256_hex(&token).into(),
157 user_id.into(),
158 kind.into(),
159 ])?
160 .run()
161 .await?;
162 Ok(token)
API and MCP server, Rust identity service, registration, site redesign163 }
164
Email verification, password reset, and Git for AI scale positioning165 /// Consumes a token of `kind`, returning its owner if it was valid.
166 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
167 let id = crypto::sha256_hex(token);
168 let owner = self
169 .db
RFC 3339 timestamps in identity and repos170 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look171 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning172 JOIN users ON users.id = email_tokens.user_id
173 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos174 AND email_tokens.expires_at > {SQL_NOW}"
175 ))
Email verification, password reset, and Git for AI scale positioning176 .bind(&[id.as_str().into(), kind.into()])?
177 .first::<TokenOwner>(None)
178 .await?;
179 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look180 // Every outstanding token of this kind dies with the one used:
181 // every reset link, and every confirmation link for the same
182 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning183 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look184 .prepare(
185 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
186 AND (?2 = 'reset' OR email_id IS ?3)",
187 )
188 .bind(&[
189 owner.id.as_str().into(),
190 kind.into(),
191 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
192 ])?
Email verification, password reset, and Git for AI scale positioning193 .run()
194 .await?;
195 }
196 Ok(owner)
197 }
198
199 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
200 let token = self
201 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
202 .await?;
203 email::send_verification(&self.env, email, &user.username, &token).await
204 }
205
206 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 look207 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
208 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
209 }
210 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning211 }
212
213 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
214 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
215 return Ok(Outcome::fail(
216 FailureCode::Invalid,
217 "This confirmation link is not valid or has expired.",
218 ));
219 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look220 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
221 return Ok(Outcome::Fail(failure));
222 }
223 // Whether the account is confirmed: whether its primary is.
224 let verified = self
225 .find_public_user(
226 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
227 &owner.id,
228 )
229 .await?
230 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning231 Ok(Outcome::Ok(User {
232 id: owner.id,
233 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 verified,
Workspaces own repositories235 ..User::default()
Email verification, password reset, and Git for AI scale positioning236 }))
237 }
238
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look239 /// Any confirmed address of an account can ask for a reset; so can the
240 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning241 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 look242 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
243 && match a.client.as_deref() {
244 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
245 None => true,
246 };
247 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 exists248 // A failure from here on happens only for a real account, so it
249 // is logged, never answered: the reply below stays the same.
250 if let Err(error) = self.send_reset(&target).await {
251 worker::console_error!("password reset for a known address failed: {error}");
252 }
253 }
254 // The same answer either way, so addresses cannot be probed.
255 Ok(true)
256 }
257
258 /// Saves a reset link for `target` and mails it, telling the account's
259 /// other addresses.
260 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
261 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look262 let token = crypto::random_hex(32);
263 self.db
264 .prepare(format!(
265 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
266 VALUES (?, ?, 'reset', {}, ?)",
267 sql_after(RESET_TTL_SECONDS)
268 ))
269 .bind(&[
270 crypto::sha256_hex(&token).into(),
271 target.user_id.as_str().into(),
272 target.email_id.as_str().into(),
273 ])?
274 .run()
Email verification, password reset, and Git for AI scale positioning275 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look276 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
277 // The primary and the backup hear of it when it went elsewhere.
278 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
279 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
280 let change = format!("A password reset was asked for through {}", target.display);
281 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
282 worker::console_error!("security notice failed: {error}");
283 }
284 }
Email verification, password reset, and Git for AI scale positioning285 }
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 exists286 Ok(())
Email verification, password reset, and Git for AI scale positioning287 }
288
289 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
290 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
291 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
292 }
293 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
294 return Ok(Outcome::fail(
295 FailureCode::Invalid,
296 "This reset link is not valid or has expired.",
297 ));
298 };
299 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look300 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning301 .bind(&[
302 crypto::hash_password(&a.password).into(),
303 owner.id.as_str().into(),
304 ])?
305 .run()
306 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look307 // Following an emailed link also proves the address it went to
308 // (unless another account confirmed it first).
309 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
310 // Anyone signed in with the old password is signed out, and nobody
311 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning312 self.db
313 .prepare("DELETE FROM sessions WHERE user_id = ?")
314 .bind(&[owner.id.as_str().into()])?
315 .run()
316 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look317 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
318 self.log_security(&owner.id, "password_changed", None, None).await;
319 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
320 let verified = self
321 .find_public_user(
322 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
323 &owner.id,
324 )
325 .await?
326 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning327 Ok(Outcome::Ok(User {
328 id: owner.id,
329 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look330 verified,
Workspaces own repositories331 ..User::default()
Email verification, password reset, and Git for AI scale positioning332 }))
333 }
334
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look335 /// The account a login names: a username, or any confirmed address.
336 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
337 let login = login.trim().to_lowercase();
338 let (column, value) = if login.contains('@') {
339 match self.user_with_verified_email(&login).await? {
340 Some(id) => ("id", id),
341 None => return Ok(None),
342 }
343 } else {
344 ("username", login)
345 };
346 self.db
347 .prepare(format!(
348 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
349 ))
350 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign351 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look352 .await
353 }
354
355 /// Checks a password for a login, throttled (see throttle.rs). The
356 /// refusal is one of two messages, the same for every account.
357 async fn checked_password(
358 &self,
359 login: &str,
360 password: &str,
361 client: Option<&str>,
362 ) -> Result<std::result::Result<User, &'static str>> {
363 let row = self.password_row(login).await?;
364 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
365 let (account_key, client_key) = Identity::password_keys(&subject, client);
366 if self.password_locked(&account_key, client_key.as_deref()).await? {
367 return Ok(Err(throttle::THROTTLED));
368 }
369 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
370 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
371 Some(row) => {
372 self.clear(&account_key).await?;
373 Ok(Ok(User {
374 id: row.id,
375 username: row.username,
376 verified: row.verified != 0,
377 ..User::default()
378 }))
379 }
380 None => {
381 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
382 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
383 Ok(Err("Incorrect username or password."))
384 }
385 }
386 }
387
388 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
389 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories390 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign391 }
392
393 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
394 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-agent395 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign396 let email = a.email.trim().to_lowercase();
397 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 look398 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
399 // The invite first: without one, nothing else on the form matters.
400 if self.invites_required() && invite_code.is_none() {
401 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
402 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent403 if !claimable {
API and MCP server, Rust identity service, registration, site redesign404 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent405 "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 redesign406 );
407 }
408 let well_formed_email = email
409 .split_once('@')
410 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
411 && !email.contains(char::is_whitespace);
412 if !well_formed_email {
413 return invalid("Enter a valid email address.");
414 }
415 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning416 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign417 }
418 let taken = self
419 .db
Agents as a team: lifecycle, merge queue, billing and a new shell420 // Usernames and workspaces share one namespace, so that a name
421 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look422 // An address is taken once an account has confirmed it; an
423 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell424 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look425 "SELECT username FROM users WHERE username = ?
426 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 shell427 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
428 )
429 .bind(&[
430 username.as_str().into(),
431 email.as_str().into(),
432 username.as_str().into(),
433 ])?
API and MCP server, Rust identity service, registration, site redesign434 .first::<serde_json::Value>(None)
435 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look436 // A renamed workspace's old slug stays reserved for it a while, and
437 // a deleted workspace's for good.
438 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign439 return Ok(Outcome::fail(
440 FailureCode::Conflict,
441 "That username or email is already registered.",
442 ));
443 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look444 let password_hash = crypto::hash_password(&a.password);
445 let user = match self
446 .create_account(invites::NewAccount {
447 username: &username,
448 email: &email,
449 password_hash: &password_hash,
450 verified: false,
451 invite_code,
452 client: a.client.as_deref(),
453 })
454 .await?
455 {
456 Outcome::Ok(user) => user,
457 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign458 };
Email verification, password reset, and Git for AI scale positioning459 // 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 quotas460 // An invite sent to this address confirmed it already (invites.rs).
461 if !user.verified
462 && let Err(error) = self.send_verification(&user, &email).await
463 {
Email verification, password reset, and Git for AI scale positioning464 worker::console_error!("verification email failed: {error}");
465 }
API and MCP server, Rust identity service, registration, site redesign466 self.start_session(user).await
467 }
468
469 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 look470 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
471 Ok(user) => user,
472 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign473 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look474 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign475 self.start_session(user).await
476 }
477
478 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
479 let session_token = crypto::random_hex(32);
480 self.db
RFC 3339 timestamps in identity and repos481 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look482 // Signing in is proof it is the person: see security.rs.
483 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos484 sql_after(SESSION_TTL_SECONDS)
485 ))
API and MCP server, Rust identity service, registration, site redesign486 .bind(&[
487 crypto::sha256_hex(&session_token).into(),
488 user.id.as_str().into(),
489 ])?
490 .run()
491 .await?;
492 Ok(Outcome::Ok(SignedIn {
493 user,
494 session_token,
495 }))
496 }
497
498 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
499 self.db
500 .prepare("DELETE FROM sessions WHERE id = ?")
501 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
502 .run()
503 .await?;
504 Ok(())
505 }
506
507 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
508 self.find_user(
RFC 3339 timestamps in identity and repos509 &format!(
Workspace names and icons, and a component kit for every control510 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
511 users.avatar
RFC 3339 timestamps in identity and repos512 FROM sessions JOIN users ON users.id = sessions.user_id
513 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
514 ),
API and MCP server, Rust identity service, registration, site redesign515 &crypto::sha256_hex(&a.session_token),
516 )
517 .await
518 }
519
520 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
521 // Like GitHub, a token alone identifies its user.
522 if a.secret.starts_with(TOKEN_PREFIX) {
523 self.user_for_access_token(&a.secret).await
524 } else {
525 self.user_for_password(&a.username, &a.secret).await
526 }
527 }
528
529 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
530 self.find_user(
Email verification, password reset, and Git for AI scale positioning531 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified FROM ssh_keys
API and MCP server, Rust identity service, registration, site redesign532 JOIN users ON users.id = ssh_keys.user_id
533 WHERE fingerprint = ?",
534 &a.fingerprint,
535 )
536 .await
537 }
538
539 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories540 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning541 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign542 &a.username.to_lowercase(),
543 )
544 .await
545 }
546
Inbox: threads, reasons, subscriptions and watching547 /// `notify_by_email`: an inbox item, emailed to the person it is for,
548 /// only at a confirmed address and only while they can still read the
549 /// repository it is about. Returns whether it was sent.
550 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
551 #[derive(Deserialize)]
552 struct Address {
553 email: Option<String>,
554 }
555 let user = self
556 .find_user(
557 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
558 &a.username.to_lowercase(),
559 )
560 .await?;
561 let Some(user) = user.filter(|user| user.verified) else {
562 return Ok(false);
563 };
564 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
565 &self.env.service("REPOS")?,
566 "readable",
567 &g1t_contracts::repos::ReadableArgs {
568 ids: vec![a.repo_id.clone()],
569 viewer: Some(user.clone()),
570 },
571 )
572 .await?;
573 if readable.is_empty() {
574 return Ok(false);
575 }
576 let address = self
577 .db
578 .prepare("SELECT email FROM users WHERE id = ?")
579 .bind(&[user.id.as_str().into()])?
580 .first::<Address>(None)
581 .await?
582 .and_then(|row| row.email)
583 .filter(|email| !email.trim().is_empty());
584 let Some(address) = address else {
585 return Ok(false);
586 };
587 email::send_notification(&self.env, &address, &a).await?;
588 Ok(true)
589 }
590
What happened across an outcome, as a feed beside its graph591 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
592 #[derive(serde::Deserialize)]
593 struct Named {
594 id: String,
595 name: String,
596 }
597 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
598 let mut names = std::collections::HashMap::new();
599 if ids.is_empty() {
600 return Ok(names);
601 }
602 let marks = vec!["?"; ids.len()].join(", ");
603 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
604 for sql in [
605 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
606 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
607 ] {
608 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
609 names.insert(row.id, row.name);
610 }
611 }
612 Ok(names)
613 }
614
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97615 /// `accounts`: the accounts behind these ids (at most 200), each with
616 /// its username and avatar, for lists that keep ids, such as who
617 /// starred a repository. Ids of no account are left out.
618 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
619 #[derive(serde::Deserialize)]
620 struct Row {
621 id: String,
622 username: String,
623 avatar: Option<String>,
624 }
625 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
626 let mut found = std::collections::HashMap::new();
627 if ids.is_empty() {
628 return Ok(found);
629 }
630 let marks = vec!["?"; ids.len()].join(", ");
631 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
632 let rows = self
633 .db
634 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
635 .bind(&bind)?
636 .all()
637 .await?
638 .results::<Row>()?;
639 for row in rows {
640 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
641 }
642 Ok(found)
643 }
644
API and MCP server, Rust identity service, registration, site redesign645 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
646 let rows = self
647 .db
648 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
649 .bind(&[a.user.id.into()])?
650 .all()
651 .await?
652 .results::<KeyRow>()?;
653 Ok(rows.into_iter().map(SshKey::from).collect())
654 }
655
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge656 /// The account (user id) that registered each key, by fingerprint
657 /// (`SHA256:…`). At most 100; unknown keys are left out.
658 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
659 #[derive(serde::Deserialize)]
660 struct Row {
661 fingerprint: String,
662 user_id: String,
663 }
664 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
665 if fingerprints.is_empty() {
666 return Ok(std::collections::HashMap::new());
667 }
668 let marks = vec!["?"; fingerprints.len()].join(", ");
669 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
670 Ok(self
671 .db
672 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
673 .bind(&binds)?
674 .all()
675 .await?
676 .results::<Row>()?
677 .into_iter()
678 .map(|row| (row.fingerprint, row.user_id))
679 .collect())
680 }
681
API and MCP server, Rust identity service, registration, site redesign682 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
683 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
684 return Ok(Outcome::fail(
685 FailureCode::Invalid,
686 "That is not a valid OpenSSH public key.",
687 ));
688 };
689 let taken = self
690 .db
691 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
692 .bind(&[key.fingerprint.as_str().into()])?
693 .first::<serde_json::Value>(None)
694 .await?;
695 if taken.is_some() {
696 return Ok(Outcome::fail(
697 FailureCode::Conflict,
698 "That key is already registered.",
699 ));
700 }
701 let now = now_ms();
702 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
703 .into_iter()
704 .find(|candidate| !candidate.is_empty())
705 .unwrap_or_default()
706 .to_owned();
707 let row = KeyRow {
708 id: new_id("key", now),
709 title,
710 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos711 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign712 };
713 self.db
714 .prepare(
715 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
716 VALUES (?, ?, ?, ?, ?, ?)",
717 )
718 .bind(&[
719 row.id.as_str().into(),
720 a.user.id.into(),
721 row.title.as_str().into(),
722 key.public_key.into(),
723 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos724 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign725 ])?
726 .run()
727 .await?;
728 Ok(Outcome::Ok(row.into()))
729 }
730
731 /// Deletes a row the user owns from `table`.
732 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
733 self.db
734 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
735 .bind(&[a.id.into(), a.user.id.into()])?
736 .run()
737 .await?;
738 Ok(())
739 }
740}
741
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas742/// 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 member743/// the last summary's window was still open, so none waits on a later one;
744/// and deleted workspaces past their restore window are purged
745/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas746#[event(scheduled)]
747async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
748 let Ok(db) = env.d1("DB") else { return };
749 let identity = Identity { db, env };
750 if let Err(error) = identity.notify_staff_of_requests().await {
751 worker::console_error!("waitlist summary: {error}");
752 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member753 if let Err(error) = identity.purge_due_workspaces().await {
754 worker::console_error!("workspace purge: {error}");
755 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas756}
757
API and MCP server, Rust identity service, registration, site redesign758#[event(fetch)]
759async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
760 let Some(method) = rpc_method(&request) else {
761 return Response::error("Not found", 404);
762 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents763 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
764 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign765 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents766 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign767
Fast pages, required checks on the branch, self-hosted runners, honest incidents768 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette769 "register" => {
770 let outcome = identity.register(args(body)?).await?;
771 if let Outcome::Ok(signed_in) = &outcome {
772 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
773 }
774 reply(&outcome)
775 }
API and MCP server, Rust identity service, registration, site redesign776 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette777 "create_workspace" => {
778 let outcome = identity.create_workspace(args(body)?).await?;
779 if let Outcome::Ok(workspace) = &outcome {
780 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
781 }
782 reply(&outcome)
783 }
Workspaces own repositories784 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
785 "list_members" => reply(&identity.list_members(args(body)?).await?),
786 "add_member" => reply(&identity.add_member(args(body)?).await?),
787 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette788 "update_workspace" => {
789 let outcome = identity.update_workspace(args(body)?).await?;
790 if let Outcome::Ok(workspace) = &outcome {
791 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
792 }
793 reply(&outcome)
794 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains795 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
796 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
797 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'798 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look799 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
800 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
801 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette802 "set_workspace_avatar" => {
803 let outcome = identity.set_workspace_avatar(args(body)?).await?;
804 if let Outcome::Ok(workspace) = &outcome {
805 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
806 }
807 reply(&outcome)
808 }
809 "set_user_avatar" => {
810 let a: SetUserAvatarArgs = args(body)?;
811 let (username, id) = (a.user.username.clone(), a.user.id.clone());
812 let outcome = identity.set_user_avatar(a).await?;
813 if matches!(outcome, Outcome::Ok(_)) {
814 identity.announce_user(&username, Some(&id)).await;
815 }
816 reply(&outcome)
817 }
Agents as a team: lifecycle, merge queue, billing and a new shell818 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
819 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
820 "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 look821 // Signing in with GitHub; see github.rs.
822 "github_enabled" => reply(&identity.github_enabled()),
823 "github_start" => reply(&identity.github_start(args(body)?).await?),
824 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
825 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
826 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
827 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
828 "github_account" => reply(&identity.github_account(args(body)?).await?),
829 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
830 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
831 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
832 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications833 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
834 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
835 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
836 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
837 "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 step838 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API839 "device_start" => reply(&identity.device_start(args(body)?).await?),
840 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
841 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
842 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning843 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
844 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
845 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
846 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look847 // A person's email addresses; see emails.rs and security.rs.
848 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
849 "add_email" => reply(&identity.add_email(args(body)?).await?),
850 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
851 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
852 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
853 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
854 "security_log" => reply(&identity.security_log(args(body)?).await?),
855 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
856 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
857 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
858 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
859 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign860 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
861 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
862 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
863 "user_for_access_token" => {
864 let a: TokenArgs = args(body)?;
865 reply(&identity.user_for_access_token(&a.token).await?)
866 }
867 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
868 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph869 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97870 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching871 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains872 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette873 "update_profile" => {
874 let outcome = identity.update_profile(args(body)?).await?;
875 if let Outcome::Ok(profile) = &outcome {
876 identity.announce_user(&profile.username, None).await;
877 }
878 reply(&outcome)
879 }
880 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains881 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign882 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge883 // Services only: who registered each key, for verifying commit
884 // signatures (repos' signatures.rs).
885 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign886 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
887 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
888 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
889 "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 step890 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity891 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
892 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
893 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
894 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
895 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
896 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
897 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
898 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell899 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
900 "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 API901 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
902 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
903 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'904 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
905 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign906 "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 look907 // Invites and the waitlist; see invites.rs.
908 "registration" => reply(&identity.registration_mode()),
909 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
910 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
911 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
912 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
913 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
914 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
915 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
916 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
917 "request_access" => reply(&identity.request_access(args(body)?).await?),
918 // Who has access to a repository; see access.rs.
919 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
920 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
921 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
922 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
923 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
924 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
925 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
926 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
927 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'928 // Where a workspace keeps its repositories' git data (EU residency).
929 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
930 "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 look931 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
932 "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 bar933 // Teams (teams.rs).
934 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
935 "get_team" => reply(&identity.get_team(args(body)?).await?),
936 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'937 "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 bar938 "update_team" => reply(&identity.update_team(args(body)?).await?),
939 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
940 "team_members" => reply(&identity.team_members(args(body)?).await?),
941 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
942 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
943 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
944 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
945 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
946 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
947 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
948 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
949 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
950 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace951 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
952 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
953 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
954 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look955 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
956 "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 quotas957 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look958 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
959 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
960 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
961 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
962 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
963 "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 member964 // Deleted workspaces, restored or purged by staff; see deletion.rs.
965 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
966 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
967 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'968 // Workspace aliases, set by staff only; see aliases.rs.
969 "admin_aliases" => reply(&identity.admin_aliases().await?),
970 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
971 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign972 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents973 };
974 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign975}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent976
977#[cfg(test)]
978mod register_tests {
979 use super::*;
980
981 #[test]
982 fn nobody_registers_as_g1t() {
983 // What register checks the username with, whatever its case.
984 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
985 assert_eq!(claimable_namespace(username), None, "{username}");
986 }
987 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
988 }
989}

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