Skip to content

g1t/services/identity/src/lib.rs

977 lines43,855 bytesCodeBlame

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

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

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