Skip to content
980 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;
Actions: keep workflow runs safe22mod 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;
Agents as a team: lifecycle, merge queue, billing and a new shell27mod tokens;
Workspaces own repositories28mod workspaces;
API and MCP server, Rust identity service, registration, site redesign29
30use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos31use 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-agent32use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign33use g1t_kit::{args, now_ms, reply, rpc_method};
34use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell35use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign36use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas37use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign38
RFC 3339 timestamps in identity and repos39const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
40const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
41const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign42const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning43const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
44
45/// A user as selected from the database; `verified` arrives as 0 or 1.
46#[derive(Deserialize)]
47struct Account {
48 id: String,
49 username: String,
50 verified: u8,
Workspace names and icons, and a component kit for every control51 /// Selected only where the person is being shown to themselves.
52 #[serde(default)]
53 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning54}
55
56impl From<Account> for User {
57 fn from(row: Account) -> Self {
58 User {
59 id: row.id,
60 username: row.username,
61 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control62 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell63 ..User::default()
Email verification, password reset, and Git for AI scale positioning64 }
65 }
66}
API and MCP server, Rust identity service, registration, site redesign67
68#[derive(Deserialize)]
69struct UserRow {
70 id: String,
71 username: String,
72 password_hash: String,
Email verification, password reset, and Git for AI scale positioning73 verified: u8,
API and MCP server, Rust identity service, registration, site redesign74}
75
Email verification, password reset, and Git for AI scale positioning76/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign77#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning78struct TokenOwner {
79 id: String,
80 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 /// The address a link was sent to; null on links from before accounts
82 /// had several, which are for the primary.
83 #[serde(default)]
84 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning85}
86
87#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign88struct KeyRow {
89 id: String,
90 title: String,
91 fingerprint: String,
RFC 3339 timestamps in identity and repos92 created_at: String,
API and MCP server, Rust identity service, registration, site redesign93}
94
95impl From<KeyRow> for SshKey {
96 fn from(row: KeyRow) -> Self {
97 SshKey {
98 id: row.id,
99 title: row.title,
100 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos101 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign102 }
103 }
104}
105
106struct Identity {
107 db: D1Database,
Email verification, password reset, and Git for AI scale positioning108 env: Env,
API and MCP server, Rust identity service, registration, site redesign109}
110
111impl Identity {
Workspaces own repositories112 /// Runs a query that returns at most one user, for showing to others:
113 /// without their workspaces.
114 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning115 Ok(self
116 .db
API and MCP server, Rust identity service, registration, site redesign117 .prepare(sql)
118 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning119 .first::<Account>(None)
120 .await?
121 .map(User::from))
122 }
123
Workspaces own repositories124 /// Attaches the workspaces a user belongs to, so that any service can
125 /// authorize them without asking again.
126 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
127 let Some(mut user) = user else {
128 return Ok(None);
129 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look130 let memberships = self.memberships(&user.id).await?;
131 // Access to a workspace is used only within its policy; see security.rs.
132 user.workspaces = self.within_policy(&user.id, memberships).await?;
133 // Roles on single repositories, under the same policy (access.rs).
134 let grants = self.grants_of(&user.id).await?;
135 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories136 Ok(Some(user))
137 }
138
139 /// Runs a query that resolves credentials to at most one user.
140 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
141 let user = self.find_public_user(sql, param).await?;
142 self.with_workspaces(user).await
143 }
144
Email verification, password reset, and Git for AI scale positioning145 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos146 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning147 let token = crypto::random_hex(32);
148 self.db
RFC 3339 timestamps in identity and repos149 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning150 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos151 VALUES (?, ?, ?, {})",
152 sql_after(ttl)
153 ))
Email verification, password reset, and Git for AI scale positioning154 .bind(&[
155 crypto::sha256_hex(&token).into(),
156 user_id.into(),
157 kind.into(),
158 ])?
159 .run()
160 .await?;
161 Ok(token)
API and MCP server, Rust identity service, registration, site redesign162 }
163
Email verification, password reset, and Git for AI scale positioning164 /// Consumes a token of `kind`, returning its owner if it was valid.
165 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
166 let id = crypto::sha256_hex(token);
167 let owner = self
168 .db
RFC 3339 timestamps in identity and repos169 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look170 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning171 JOIN users ON users.id = email_tokens.user_id
172 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos173 AND email_tokens.expires_at > {SQL_NOW}"
174 ))
Email verification, password reset, and Git for AI scale positioning175 .bind(&[id.as_str().into(), kind.into()])?
176 .first::<TokenOwner>(None)
177 .await?;
178 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look179 // Every outstanding token of this kind dies with the one used:
180 // every reset link, and every confirmation link for the same
181 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning182 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look183 .prepare(
184 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
185 AND (?2 = 'reset' OR email_id IS ?3)",
186 )
187 .bind(&[
188 owner.id.as_str().into(),
189 kind.into(),
190 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
191 ])?
Email verification, password reset, and Git for AI scale positioning192 .run()
193 .await?;
194 }
195 Ok(owner)
196 }
197
198 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
199 let token = self
200 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
201 .await?;
202 email::send_verification(&self.env, email, &user.username, &token).await
203 }
204
205 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 look206 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
207 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
208 }
209 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning210 }
211
212 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
213 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
214 return Ok(Outcome::fail(
215 FailureCode::Invalid,
216 "This confirmation link is not valid or has expired.",
217 ));
218 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look219 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
220 return Ok(Outcome::Fail(failure));
221 }
222 // Whether the account is confirmed: whether its primary is.
223 let verified = self
224 .find_public_user(
225 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
226 &owner.id,
227 )
228 .await?
229 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning230 Ok(Outcome::Ok(User {
231 id: owner.id,
232 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look233 verified,
Workspaces own repositories234 ..User::default()
Email verification, password reset, and Git for AI scale positioning235 }))
236 }
237
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look238 /// Any confirmed address of an account can ask for a reset; so can the
239 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning240 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 look241 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
242 && match a.client.as_deref() {
243 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
244 None => true,
245 };
246 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 exists247 // A failure from here on happens only for a real account, so it
248 // is logged, never answered: the reply below stays the same.
249 if let Err(error) = self.send_reset(&target).await {
250 worker::console_error!("password reset for a known address failed: {error}");
251 }
252 }
253 // The same answer either way, so addresses cannot be probed.
254 Ok(true)
255 }
256
257 /// Saves a reset link for `target` and mails it, telling the account's
258 /// other addresses.
259 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
260 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look261 let token = crypto::random_hex(32);
262 self.db
263 .prepare(format!(
264 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
265 VALUES (?, ?, 'reset', {}, ?)",
266 sql_after(RESET_TTL_SECONDS)
267 ))
268 .bind(&[
269 crypto::sha256_hex(&token).into(),
270 target.user_id.as_str().into(),
271 target.email_id.as_str().into(),
272 ])?
273 .run()
Email verification, password reset, and Git for AI scale positioning274 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look275 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
276 // The primary and the backup hear of it when it went elsewhere.
277 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
278 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
279 let change = format!("A password reset was asked for through {}", target.display);
280 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
281 worker::console_error!("security notice failed: {error}");
282 }
283 }
Email verification, password reset, and Git for AI scale positioning284 }
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 exists285 Ok(())
Email verification, password reset, and Git for AI scale positioning286 }
287
288 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
289 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
290 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
291 }
292 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
293 return Ok(Outcome::fail(
294 FailureCode::Invalid,
295 "This reset link is not valid or has expired.",
296 ));
297 };
298 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look299 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning300 .bind(&[
301 crypto::hash_password(&a.password).into(),
302 owner.id.as_str().into(),
303 ])?
304 .run()
305 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look306 // Following an emailed link also proves the address it went to
307 // (unless another account confirmed it first).
308 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
309 // Anyone signed in with the old password is signed out, and nobody
310 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning311 self.db
312 .prepare("DELETE FROM sessions WHERE user_id = ?")
313 .bind(&[owner.id.as_str().into()])?
314 .run()
315 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
317 self.log_security(&owner.id, "password_changed", None, None).await;
318 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
319 let verified = self
320 .find_public_user(
321 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
322 &owner.id,
323 )
324 .await?
325 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning326 Ok(Outcome::Ok(User {
327 id: owner.id,
328 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look329 verified,
Workspaces own repositories330 ..User::default()
Email verification, password reset, and Git for AI scale positioning331 }))
332 }
333
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look334 /// The account a login names: a username, or any confirmed address.
335 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
336 let login = login.trim().to_lowercase();
337 let (column, value) = if login.contains('@') {
338 match self.user_with_verified_email(&login).await? {
339 Some(id) => ("id", id),
340 None => return Ok(None),
341 }
342 } else {
343 ("username", login)
344 };
345 self.db
346 .prepare(format!(
347 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
348 ))
349 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign350 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look351 .await
352 }
353
354 /// Checks a password for a login, throttled (see throttle.rs). The
355 /// refusal is one of two messages, the same for every account.
356 async fn checked_password(
357 &self,
358 login: &str,
359 password: &str,
360 client: Option<&str>,
361 ) -> Result<std::result::Result<User, &'static str>> {
362 let row = self.password_row(login).await?;
363 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
364 let (account_key, client_key) = Identity::password_keys(&subject, client);
365 if self.password_locked(&account_key, client_key.as_deref()).await? {
366 return Ok(Err(throttle::THROTTLED));
367 }
368 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
369 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
370 Some(row) => {
371 self.clear(&account_key).await?;
372 Ok(Ok(User {
373 id: row.id,
374 username: row.username,
375 verified: row.verified != 0,
376 ..User::default()
377 }))
378 }
379 None => {
380 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
381 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
382 Ok(Err("Incorrect username or password."))
383 }
384 }
385 }
386
387 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
388 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories389 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign390 }
391
392 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
393 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-agent394 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign395 let email = a.email.trim().to_lowercase();
396 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 look397 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
398 // The invite first: without one, nothing else on the form matters.
399 if self.invites_required() && invite_code.is_none() {
400 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
401 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent402 if !claimable {
API and MCP server, Rust identity service, registration, site redesign403 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent404 "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 redesign405 );
406 }
407 let well_formed_email = email
408 .split_once('@')
409 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
410 && !email.contains(char::is_whitespace);
411 if !well_formed_email {
412 return invalid("Enter a valid email address.");
413 }
414 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning415 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign416 }
417 let taken = self
418 .db
Agents as a team: lifecycle, merge queue, billing and a new shell419 // Usernames and workspaces share one namespace, so that a name
420 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look421 // An address is taken once an account has confirmed it; an
422 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell423 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look424 "SELECT username FROM users WHERE username = ?
425 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 shell426 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
427 )
428 .bind(&[
429 username.as_str().into(),
430 email.as_str().into(),
431 username.as_str().into(),
432 ])?
API and MCP server, Rust identity service, registration, site redesign433 .first::<serde_json::Value>(None)
434 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look435 // A renamed workspace's old slug stays reserved for it a while, and
436 // a deleted workspace's for good.
437 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign438 return Ok(Outcome::fail(
439 FailureCode::Conflict,
440 "That username or email is already registered.",
441 ));
442 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look443 let password_hash = crypto::hash_password(&a.password);
444 let user = match self
445 .create_account(invites::NewAccount {
446 username: &username,
447 email: &email,
448 password_hash: &password_hash,
449 verified: false,
450 invite_code,
451 client: a.client.as_deref(),
452 })
453 .await?
454 {
455 Outcome::Ok(user) => user,
456 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign457 };
Email verification, password reset, and Git for AI scale positioning458 // 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 quotas459 // An invite sent to this address confirmed it already (invites.rs).
460 if !user.verified
461 && let Err(error) = self.send_verification(&user, &email).await
462 {
Email verification, password reset, and Git for AI scale positioning463 worker::console_error!("verification email failed: {error}");
464 }
API and MCP server, Rust identity service, registration, site redesign465 self.start_session(user).await
466 }
467
468 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 look469 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
470 Ok(user) => user,
471 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign472 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look473 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign474 self.start_session(user).await
475 }
476
477 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
478 let session_token = crypto::random_hex(32);
479 self.db
RFC 3339 timestamps in identity and repos480 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look481 // Signing in is proof it is the person: see security.rs.
482 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos483 sql_after(SESSION_TTL_SECONDS)
484 ))
API and MCP server, Rust identity service, registration, site redesign485 .bind(&[
486 crypto::sha256_hex(&session_token).into(),
487 user.id.as_str().into(),
488 ])?
489 .run()
490 .await?;
491 Ok(Outcome::Ok(SignedIn {
492 user,
493 session_token,
494 }))
495 }
496
497 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
498 self.db
499 .prepare("DELETE FROM sessions WHERE id = ?")
500 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
501 .run()
502 .await?;
503 Ok(())
504 }
505
506 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
507 self.find_user(
RFC 3339 timestamps in identity and repos508 &format!(
Workspace names and icons, and a component kit for every control509 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
510 users.avatar
RFC 3339 timestamps in identity and repos511 FROM sessions JOIN users ON users.id = sessions.user_id
512 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
513 ),
API and MCP server, Rust identity service, registration, site redesign514 &crypto::sha256_hex(&a.session_token),
515 )
516 .await
517 }
518
519 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
520 // Like GitHub, a token alone identifies its user.
521 if a.secret.starts_with(TOKEN_PREFIX) {
522 self.user_for_access_token(&a.secret).await
523 } else {
524 self.user_for_password(&a.username, &a.secret).await
525 }
526 }
527
528 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
529 self.find_user(
Email verification, password reset, and Git for AI scale positioning530 "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 redesign531 JOIN users ON users.id = ssh_keys.user_id
532 WHERE fingerprint = ?",
533 &a.fingerprint,
534 )
535 .await
536 }
537
538 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories539 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning540 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign541 &a.username.to_lowercase(),
542 )
543 .await
544 }
545
Inbox: threads, reasons, subscriptions and watching546 /// `notify_by_email`: an inbox item, emailed to the person it is for,
547 /// only at a confirmed address and only while they can still read the
548 /// repository it is about. Returns whether it was sent.
549 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
550 #[derive(Deserialize)]
551 struct Address {
552 email: Option<String>,
553 }
554 let user = self
555 .find_user(
556 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
557 &a.username.to_lowercase(),
558 )
559 .await?;
560 let Some(user) = user.filter(|user| user.verified) else {
561 return Ok(false);
562 };
563 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
564 &self.env.service("REPOS")?,
565 "readable",
566 &g1t_contracts::repos::ReadableArgs {
567 ids: vec![a.repo_id.clone()],
568 viewer: Some(user.clone()),
569 },
570 )
571 .await?;
572 if readable.is_empty() {
573 return Ok(false);
574 }
575 let address = self
576 .db
577 .prepare("SELECT email FROM users WHERE id = ?")
578 .bind(&[user.id.as_str().into()])?
579 .first::<Address>(None)
580 .await?
581 .and_then(|row| row.email)
582 .filter(|email| !email.trim().is_empty());
583 let Some(address) = address else {
584 return Ok(false);
585 };
586 email::send_notification(&self.env, &address, &a).await?;
587 Ok(true)
588 }
589
What happened across an outcome, as a feed beside its graph590 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
591 #[derive(serde::Deserialize)]
592 struct Named {
593 id: String,
594 name: String,
595 }
596 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
597 let mut names = std::collections::HashMap::new();
598 if ids.is_empty() {
599 return Ok(names);
600 }
601 let marks = vec!["?"; ids.len()].join(", ");
602 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
603 for sql in [
604 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
605 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
606 ] {
607 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
608 names.insert(row.id, row.name);
609 }
610 }
611 Ok(names)
612 }
613
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97614 /// `accounts`: the accounts behind these ids (at most 200), each with
615 /// its username and avatar, for lists that keep ids, such as who
616 /// starred a repository. Ids of no account are left out.
617 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
618 #[derive(serde::Deserialize)]
619 struct Row {
620 id: String,
621 username: String,
622 avatar: Option<String>,
623 }
624 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
625 let mut found = std::collections::HashMap::new();
626 if ids.is_empty() {
627 return Ok(found);
628 }
629 let marks = vec!["?"; ids.len()].join(", ");
630 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
631 let rows = self
632 .db
633 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
634 .bind(&bind)?
635 .all()
636 .await?
637 .results::<Row>()?;
638 for row in rows {
639 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
640 }
641 Ok(found)
642 }
643
API and MCP server, Rust identity service, registration, site redesign644 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
645 let rows = self
646 .db
647 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
648 .bind(&[a.user.id.into()])?
649 .all()
650 .await?
651 .results::<KeyRow>()?;
652 Ok(rows.into_iter().map(SshKey::from).collect())
653 }
654
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge655 /// The account (user id) that registered each key, by fingerprint
656 /// (`SHA256:…`). At most 100; unknown keys are left out.
657 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
658 #[derive(serde::Deserialize)]
659 struct Row {
660 fingerprint: String,
661 user_id: String,
662 }
663 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
664 if fingerprints.is_empty() {
665 return Ok(std::collections::HashMap::new());
666 }
667 let marks = vec!["?"; fingerprints.len()].join(", ");
668 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
669 Ok(self
670 .db
671 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
672 .bind(&binds)?
673 .all()
674 .await?
675 .results::<Row>()?
676 .into_iter()
677 .map(|row| (row.fingerprint, row.user_id))
678 .collect())
679 }
680
API and MCP server, Rust identity service, registration, site redesign681 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
682 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
683 return Ok(Outcome::fail(
684 FailureCode::Invalid,
685 "That is not a valid OpenSSH public key.",
686 ));
687 };
688 let taken = self
689 .db
690 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
691 .bind(&[key.fingerprint.as_str().into()])?
692 .first::<serde_json::Value>(None)
693 .await?;
694 if taken.is_some() {
695 return Ok(Outcome::fail(
696 FailureCode::Conflict,
697 "That key is already registered.",
698 ));
699 }
700 let now = now_ms();
701 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
702 .into_iter()
703 .find(|candidate| !candidate.is_empty())
704 .unwrap_or_default()
705 .to_owned();
706 let row = KeyRow {
707 id: new_id("key", now),
708 title,
709 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos710 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign711 };
712 self.db
713 .prepare(
714 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
715 VALUES (?, ?, ?, ?, ?, ?)",
716 )
717 .bind(&[
718 row.id.as_str().into(),
719 a.user.id.into(),
720 row.title.as_str().into(),
721 key.public_key.into(),
722 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos723 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign724 ])?
725 .run()
726 .await?;
727 Ok(Outcome::Ok(row.into()))
728 }
729
730 /// Deletes a row the user owns from `table`.
731 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
732 self.db
733 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
734 .bind(&[a.id.into(), a.user.id.into()])?
735 .run()
736 .await?;
737 Ok(())
738 }
739}
740
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas741/// 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 member742/// the last summary's window was still open, so none waits on a later one;
743/// and deleted workspaces past their restore window are purged
744/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas745#[event(scheduled)]
746async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
747 let Ok(db) = env.d1("DB") else { return };
748 let identity = Identity { db, env };
749 if let Err(error) = identity.notify_staff_of_requests().await {
750 worker::console_error!("waitlist summary: {error}");
751 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member752 if let Err(error) = identity.purge_due_workspaces().await {
753 worker::console_error!("workspace purge: {error}");
754 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas755}
756
API and MCP server, Rust identity service, registration, site redesign757#[event(fetch)]
758async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
759 let Some(method) = rpc_method(&request) else {
760 return Response::error("Not found", 404);
761 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents762 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
763 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign764 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents765 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign766
Fast pages, required checks on the branch, self-hosted runners, honest incidents767 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette768 "register" => {
769 let outcome = identity.register(args(body)?).await?;
770 if let Outcome::Ok(signed_in) = &outcome {
771 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
772 }
773 reply(&outcome)
774 }
API and MCP server, Rust identity service, registration, site redesign775 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette776 "create_workspace" => {
777 let outcome = identity.create_workspace(args(body)?).await?;
778 if let Outcome::Ok(workspace) = &outcome {
779 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
780 }
781 reply(&outcome)
782 }
Workspaces own repositories783 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
784 "list_members" => reply(&identity.list_members(args(body)?).await?),
785 "add_member" => reply(&identity.add_member(args(body)?).await?),
786 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette787 "update_workspace" => {
788 let outcome = identity.update_workspace(args(body)?).await?;
789 if let Outcome::Ok(workspace) = &outcome {
790 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
791 }
792 reply(&outcome)
793 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains794 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
795 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
796 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'797 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look798 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
799 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
800 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette801 "set_workspace_avatar" => {
802 let outcome = identity.set_workspace_avatar(args(body)?).await?;
803 if let Outcome::Ok(workspace) = &outcome {
804 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
805 }
806 reply(&outcome)
807 }
808 "set_user_avatar" => {
809 let a: SetUserAvatarArgs = args(body)?;
810 let (username, id) = (a.user.username.clone(), a.user.id.clone());
811 let outcome = identity.set_user_avatar(a).await?;
812 if matches!(outcome, Outcome::Ok(_)) {
813 identity.announce_user(&username, Some(&id)).await;
814 }
815 reply(&outcome)
816 }
Agents as a team: lifecycle, merge queue, billing and a new shell817 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
818 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
819 "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 look820 // Signing in with GitHub; see github.rs.
821 "github_enabled" => reply(&identity.github_enabled()),
822 "github_start" => reply(&identity.github_start(args(body)?).await?),
823 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
824 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
825 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
826 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
827 "github_account" => reply(&identity.github_account(args(body)?).await?),
828 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
829 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
830 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
831 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications832 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
833 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
834 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
835 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
836 "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 step837 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API838 "device_start" => reply(&identity.device_start(args(body)?).await?),
839 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
840 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
841 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning842 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
843 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
844 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
845 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look846 // A person's email addresses; see emails.rs and security.rs.
847 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
848 "add_email" => reply(&identity.add_email(args(body)?).await?),
849 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
850 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
851 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
852 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
853 "security_log" => reply(&identity.security_log(args(body)?).await?),
854 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
855 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
856 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
857 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
858 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign859 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
860 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
861 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
862 "user_for_access_token" => {
863 let a: TokenArgs = args(body)?;
864 reply(&identity.user_for_access_token(&a.token).await?)
865 }
866 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
867 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph868 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97869 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching870 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains871 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette872 "update_profile" => {
873 let outcome = identity.update_profile(args(body)?).await?;
874 if let Outcome::Ok(profile) = &outcome {
875 identity.announce_user(&profile.username, None).await;
876 }
877 reply(&outcome)
878 }
879 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains880 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign881 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge882 // Services only: who registered each key, for verifying commit
883 // signatures (repos' signatures.rs).
884 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign885 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
886 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
887 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
888 "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 step889 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell890 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
891 "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 API892 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
893 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
894 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Actions: keep workflow runs safe895 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
896 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign897 "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 look898 // Invites and the waitlist; see invites.rs.
899 "registration" => reply(&identity.registration_mode()),
900 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
901 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
902 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
903 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
904 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
905 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
906 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
907 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
908 "request_access" => reply(&identity.request_access(args(body)?).await?),
909 // Who has access to a repository; see access.rs.
910 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
911 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
912 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
913 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
914 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
915 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
916 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
917 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
918 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'919 // Where a workspace keeps its repositories' git data (EU residency).
920 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
921 "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 look922 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
923 "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 bar924 // Teams (teams.rs).
925 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
926 "get_team" => reply(&identity.get_team(args(body)?).await?),
927 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'928 "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 bar929 "update_team" => reply(&identity.update_team(args(body)?).await?),
930 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
931 "team_members" => reply(&identity.team_members(args(body)?).await?),
932 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
933 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
934 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
935 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
936 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
937 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
938 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
939 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
940 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
941 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace942 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
943 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
944 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
945 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look946 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
947 "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 quotas948 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look949 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
950 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
951 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
952 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
953 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
954 "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 member955 // Deleted workspaces, restored or purged by staff; see deletion.rs.
956 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
957 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
958 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'959 // Workspace aliases, set by staff only; see aliases.rs.
960 "admin_aliases" => reply(&identity.admin_aliases().await?),
961 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
962 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign963 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents964 };
965 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign966}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent967
968#[cfg(test)]
969mod register_tests {
970 use super::*;
971
972 #[test]
973 fn nobody_registers_as_g1t() {
974 // What register checks the username with, whatever its case.
975 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
976 assert_eq!(claimable_namespace(username), None, "{username}");
977 }
978 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
979 }
980}

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