Skip to content

g1t/services/identity/src/lib.rs

909 lines40,726 bytesCodeBlame

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

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

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