Skip to content

g1t/services/identity/src/lib.rs

891 lines39,440 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;
22mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell23mod tokens;
Workspaces own repositories24mod workspaces;
API and MCP server, Rust identity service, registration, site redesign25
26use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos27use 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-agent28use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign29use g1t_kit::{args, now_ms, reply, rpc_method};
30use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell31use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign32use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas33use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign34
RFC 3339 timestamps in identity and repos35const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
36const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
37const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign38const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning39const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
40
41/// A user as selected from the database; `verified` arrives as 0 or 1.
42#[derive(Deserialize)]
43struct Account {
44 id: String,
45 username: String,
46 verified: u8,
Workspace names and icons, and a component kit for every control47 /// Selected only where the person is being shown to themselves.
48 #[serde(default)]
49 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning50}
51
52impl From<Account> for User {
53 fn from(row: Account) -> Self {
54 User {
55 id: row.id,
56 username: row.username,
57 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control58 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell59 ..User::default()
Email verification, password reset, and Git for AI scale positioning60 }
61 }
62}
API and MCP server, Rust identity service, registration, site redesign63
64#[derive(Deserialize)]
65struct UserRow {
66 id: String,
67 username: String,
68 password_hash: String,
Email verification, password reset, and Git for AI scale positioning69 verified: u8,
API and MCP server, Rust identity service, registration, site redesign70}
71
Email verification, password reset, and Git for AI scale positioning72/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign73#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning74struct TokenOwner {
75 id: String,
76 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77 /// The address a link was sent to; null on links from before accounts
78 /// had several, which are for the primary.
79 #[serde(default)]
80 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning81}
82
83#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign84struct KeyRow {
85 id: String,
86 title: String,
87 fingerprint: String,
RFC 3339 timestamps in identity and repos88 created_at: String,
API and MCP server, Rust identity service, registration, site redesign89}
90
91impl From<KeyRow> for SshKey {
92 fn from(row: KeyRow) -> Self {
93 SshKey {
94 id: row.id,
95 title: row.title,
96 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos97 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign98 }
99 }
100}
101
102struct Identity {
103 db: D1Database,
Email verification, password reset, and Git for AI scale positioning104 env: Env,
API and MCP server, Rust identity service, registration, site redesign105}
106
107impl Identity {
Workspaces own repositories108 /// Runs a query that returns at most one user, for showing to others:
109 /// without their workspaces.
110 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning111 Ok(self
112 .db
API and MCP server, Rust identity service, registration, site redesign113 .prepare(sql)
114 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning115 .first::<Account>(None)
116 .await?
117 .map(User::from))
118 }
119
Workspaces own repositories120 /// Attaches the workspaces a user belongs to, so that any service can
121 /// authorize them without asking again.
122 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
123 let Some(mut user) = user else {
124 return Ok(None);
125 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look126 let memberships = self.memberships(&user.id).await?;
127 // Access to a workspace is used only within its policy; see security.rs.
128 user.workspaces = self.within_policy(&user.id, memberships).await?;
129 // Roles on single repositories, under the same policy (access.rs).
130 let grants = self.grants_of(&user.id).await?;
131 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories132 Ok(Some(user))
133 }
134
135 /// Runs a query that resolves credentials to at most one user.
136 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
137 let user = self.find_public_user(sql, param).await?;
138 self.with_workspaces(user).await
139 }
140
Email verification, password reset, and Git for AI scale positioning141 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos142 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning143 let token = crypto::random_hex(32);
144 self.db
RFC 3339 timestamps in identity and repos145 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning146 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos147 VALUES (?, ?, ?, {})",
148 sql_after(ttl)
149 ))
Email verification, password reset, and Git for AI scale positioning150 .bind(&[
151 crypto::sha256_hex(&token).into(),
152 user_id.into(),
153 kind.into(),
154 ])?
155 .run()
156 .await?;
157 Ok(token)
API and MCP server, Rust identity service, registration, site redesign158 }
159
Email verification, password reset, and Git for AI scale positioning160 /// Consumes a token of `kind`, returning its owner if it was valid.
161 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
162 let id = crypto::sha256_hex(token);
163 let owner = self
164 .db
RFC 3339 timestamps in identity and repos165 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look166 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning167 JOIN users ON users.id = email_tokens.user_id
168 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos169 AND email_tokens.expires_at > {SQL_NOW}"
170 ))
Email verification, password reset, and Git for AI scale positioning171 .bind(&[id.as_str().into(), kind.into()])?
172 .first::<TokenOwner>(None)
173 .await?;
174 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look175 // Every outstanding token of this kind dies with the one used:
176 // every reset link, and every confirmation link for the same
177 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning178 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look179 .prepare(
180 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
181 AND (?2 = 'reset' OR email_id IS ?3)",
182 )
183 .bind(&[
184 owner.id.as_str().into(),
185 kind.into(),
186 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
187 ])?
Email verification, password reset, and Git for AI scale positioning188 .run()
189 .await?;
190 }
191 Ok(owner)
192 }
193
194 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
195 let token = self
196 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
197 .await?;
198 email::send_verification(&self.env, email, &user.username, &token).await
199 }
200
201 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 look202 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
203 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
204 }
205 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning206 }
207
208 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
209 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
210 return Ok(Outcome::fail(
211 FailureCode::Invalid,
212 "This confirmation link is not valid or has expired.",
213 ));
214 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
216 return Ok(Outcome::Fail(failure));
217 }
218 // Whether the account is confirmed: whether its primary is.
219 let verified = self
220 .find_public_user(
221 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
222 &owner.id,
223 )
224 .await?
225 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning226 Ok(Outcome::Ok(User {
227 id: owner.id,
228 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look229 verified,
Workspaces own repositories230 ..User::default()
Email verification, password reset, and Git for AI scale positioning231 }))
232 }
233
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 /// Any confirmed address of an account can ask for a reset; so can the
235 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning236 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 look237 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
238 && match a.client.as_deref() {
239 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
240 None => true,
241 };
242 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 exists243 // A failure from here on happens only for a real account, so it
244 // is logged, never answered: the reply below stays the same.
245 if let Err(error) = self.send_reset(&target).await {
246 worker::console_error!("password reset for a known address failed: {error}");
247 }
248 }
249 // The same answer either way, so addresses cannot be probed.
250 Ok(true)
251 }
252
253 /// Saves a reset link for `target` and mails it, telling the account's
254 /// other addresses.
255 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
256 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look257 let token = crypto::random_hex(32);
258 self.db
259 .prepare(format!(
260 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
261 VALUES (?, ?, 'reset', {}, ?)",
262 sql_after(RESET_TTL_SECONDS)
263 ))
264 .bind(&[
265 crypto::sha256_hex(&token).into(),
266 target.user_id.as_str().into(),
267 target.email_id.as_str().into(),
268 ])?
269 .run()
Email verification, password reset, and Git for AI scale positioning270 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look271 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
272 // The primary and the backup hear of it when it went elsewhere.
273 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
274 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
275 let change = format!("A password reset was asked for through {}", target.display);
276 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
277 worker::console_error!("security notice failed: {error}");
278 }
279 }
Email verification, password reset, and Git for AI scale positioning280 }
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 exists281 Ok(())
Email verification, password reset, and Git for AI scale positioning282 }
283
284 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
285 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
286 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
287 }
288 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
289 return Ok(Outcome::fail(
290 FailureCode::Invalid,
291 "This reset link is not valid or has expired.",
292 ));
293 };
294 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look295 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning296 .bind(&[
297 crypto::hash_password(&a.password).into(),
298 owner.id.as_str().into(),
299 ])?
300 .run()
301 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look302 // Following an emailed link also proves the address it went to
303 // (unless another account confirmed it first).
304 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
305 // Anyone signed in with the old password is signed out, and nobody
306 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning307 self.db
308 .prepare("DELETE FROM sessions WHERE user_id = ?")
309 .bind(&[owner.id.as_str().into()])?
310 .run()
311 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look312 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
313 self.log_security(&owner.id, "password_changed", None, None).await;
314 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
315 let verified = self
316 .find_public_user(
317 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
318 &owner.id,
319 )
320 .await?
321 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning322 Ok(Outcome::Ok(User {
323 id: owner.id,
324 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look325 verified,
Workspaces own repositories326 ..User::default()
Email verification, password reset, and Git for AI scale positioning327 }))
328 }
329
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look330 /// The account a login names: a username, or any confirmed address.
331 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
332 let login = login.trim().to_lowercase();
333 let (column, value) = if login.contains('@') {
334 match self.user_with_verified_email(&login).await? {
335 Some(id) => ("id", id),
336 None => return Ok(None),
337 }
338 } else {
339 ("username", login)
340 };
341 self.db
342 .prepare(format!(
343 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
344 ))
345 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign346 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look347 .await
348 }
349
350 /// Checks a password for a login, throttled (see throttle.rs). The
351 /// refusal is one of two messages, the same for every account.
352 async fn checked_password(
353 &self,
354 login: &str,
355 password: &str,
356 client: Option<&str>,
357 ) -> Result<std::result::Result<User, &'static str>> {
358 let row = self.password_row(login).await?;
359 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
360 let (account_key, client_key) = Identity::password_keys(&subject, client);
361 if self.password_locked(&account_key, client_key.as_deref()).await? {
362 return Ok(Err(throttle::THROTTLED));
363 }
364 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
365 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
366 Some(row) => {
367 self.clear(&account_key).await?;
368 Ok(Ok(User {
369 id: row.id,
370 username: row.username,
371 verified: row.verified != 0,
372 ..User::default()
373 }))
374 }
375 None => {
376 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
377 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
378 Ok(Err("Incorrect username or password."))
379 }
380 }
381 }
382
383 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
384 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories385 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign386 }
387
388 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
389 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-agent390 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign391 let email = a.email.trim().to_lowercase();
392 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 look393 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
394 // The invite first: without one, nothing else on the form matters.
395 if self.invites_required() && invite_code.is_none() {
396 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
397 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent398 if !claimable {
API and MCP server, Rust identity service, registration, site redesign399 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent400 "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 redesign401 );
402 }
403 let well_formed_email = email
404 .split_once('@')
405 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
406 && !email.contains(char::is_whitespace);
407 if !well_formed_email {
408 return invalid("Enter a valid email address.");
409 }
410 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning411 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign412 }
413 let taken = self
414 .db
Agents as a team: lifecycle, merge queue, billing and a new shell415 // Usernames and workspaces share one namespace, so that a name
416 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look417 // An address is taken once an account has confirmed it; an
418 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell419 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look420 "SELECT username FROM users WHERE username = ?
421 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 shell422 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
423 )
424 .bind(&[
425 username.as_str().into(),
426 email.as_str().into(),
427 username.as_str().into(),
428 ])?
API and MCP server, Rust identity service, registration, site redesign429 .first::<serde_json::Value>(None)
430 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look431 // A renamed workspace's old slug stays reserved for it a while, and
432 // a deleted workspace's for good.
433 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign434 return Ok(Outcome::fail(
435 FailureCode::Conflict,
436 "That username or email is already registered.",
437 ));
438 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look439 let password_hash = crypto::hash_password(&a.password);
440 let user = match self
441 .create_account(invites::NewAccount {
442 username: &username,
443 email: &email,
444 password_hash: &password_hash,
445 verified: false,
446 invite_code,
447 client: a.client.as_deref(),
448 })
449 .await?
450 {
451 Outcome::Ok(user) => user,
452 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign453 };
Email verification, password reset, and Git for AI scale positioning454 // 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 quotas455 // An invite sent to this address confirmed it already (invites.rs).
456 if !user.verified
457 && let Err(error) = self.send_verification(&user, &email).await
458 {
Email verification, password reset, and Git for AI scale positioning459 worker::console_error!("verification email failed: {error}");
460 }
API and MCP server, Rust identity service, registration, site redesign461 self.start_session(user).await
462 }
463
464 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 look465 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
466 Ok(user) => user,
467 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign468 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look469 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign470 self.start_session(user).await
471 }
472
473 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
474 let session_token = crypto::random_hex(32);
475 self.db
RFC 3339 timestamps in identity and repos476 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look477 // Signing in is proof it is the person: see security.rs.
478 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos479 sql_after(SESSION_TTL_SECONDS)
480 ))
API and MCP server, Rust identity service, registration, site redesign481 .bind(&[
482 crypto::sha256_hex(&session_token).into(),
483 user.id.as_str().into(),
484 ])?
485 .run()
486 .await?;
487 Ok(Outcome::Ok(SignedIn {
488 user,
489 session_token,
490 }))
491 }
492
493 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
494 self.db
495 .prepare("DELETE FROM sessions WHERE id = ?")
496 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
497 .run()
498 .await?;
499 Ok(())
500 }
501
502 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
503 self.find_user(
RFC 3339 timestamps in identity and repos504 &format!(
Workspace names and icons, and a component kit for every control505 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
506 users.avatar
RFC 3339 timestamps in identity and repos507 FROM sessions JOIN users ON users.id = sessions.user_id
508 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
509 ),
API and MCP server, Rust identity service, registration, site redesign510 &crypto::sha256_hex(&a.session_token),
511 )
512 .await
513 }
514
515 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
516 // Like GitHub, a token alone identifies its user.
517 if a.secret.starts_with(TOKEN_PREFIX) {
518 self.user_for_access_token(&a.secret).await
519 } else {
520 self.user_for_password(&a.username, &a.secret).await
521 }
522 }
523
524 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
525 self.find_user(
Email verification, password reset, and Git for AI scale positioning526 "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 redesign527 JOIN users ON users.id = ssh_keys.user_id
528 WHERE fingerprint = ?",
529 &a.fingerprint,
530 )
531 .await
532 }
533
534 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories535 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning536 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign537 &a.username.to_lowercase(),
538 )
539 .await
540 }
541
Inbox: threads, reasons, subscriptions and watching542 /// `notify_by_email`: an inbox item, emailed to the person it is for,
543 /// only at a confirmed address and only while they can still read the
544 /// repository it is about. Returns whether it was sent.
545 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
546 #[derive(Deserialize)]
547 struct Address {
548 email: Option<String>,
549 }
550 let user = self
551 .find_user(
552 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
553 &a.username.to_lowercase(),
554 )
555 .await?;
556 let Some(user) = user.filter(|user| user.verified) else {
557 return Ok(false);
558 };
559 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
560 &self.env.service("REPOS")?,
561 "readable",
562 &g1t_contracts::repos::ReadableArgs {
563 ids: vec![a.repo_id.clone()],
564 viewer: Some(user.clone()),
565 },
566 )
567 .await?;
568 if readable.is_empty() {
569 return Ok(false);
570 }
571 let address = self
572 .db
573 .prepare("SELECT email FROM users WHERE id = ?")
574 .bind(&[user.id.as_str().into()])?
575 .first::<Address>(None)
576 .await?
577 .and_then(|row| row.email)
578 .filter(|email| !email.trim().is_empty());
579 let Some(address) = address else {
580 return Ok(false);
581 };
582 email::send_notification(&self.env, &address, &a).await?;
583 Ok(true)
584 }
585
What happened across an outcome, as a feed beside its graph586 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
587 #[derive(serde::Deserialize)]
588 struct Named {
589 id: String,
590 name: String,
591 }
592 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
593 let mut names = std::collections::HashMap::new();
594 if ids.is_empty() {
595 return Ok(names);
596 }
597 let marks = vec!["?"; ids.len()].join(", ");
598 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
599 for sql in [
600 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
601 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
602 ] {
603 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
604 names.insert(row.id, row.name);
605 }
606 }
607 Ok(names)
608 }
609
API and MCP server, Rust identity service, registration, site redesign610 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
611 let rows = self
612 .db
613 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
614 .bind(&[a.user.id.into()])?
615 .all()
616 .await?
617 .results::<KeyRow>()?;
618 Ok(rows.into_iter().map(SshKey::from).collect())
619 }
620
621 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
622 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
623 return Ok(Outcome::fail(
624 FailureCode::Invalid,
625 "That is not a valid OpenSSH public key.",
626 ));
627 };
628 let taken = self
629 .db
630 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
631 .bind(&[key.fingerprint.as_str().into()])?
632 .first::<serde_json::Value>(None)
633 .await?;
634 if taken.is_some() {
635 return Ok(Outcome::fail(
636 FailureCode::Conflict,
637 "That key is already registered.",
638 ));
639 }
640 let now = now_ms();
641 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
642 .into_iter()
643 .find(|candidate| !candidate.is_empty())
644 .unwrap_or_default()
645 .to_owned();
646 let row = KeyRow {
647 id: new_id("key", now),
648 title,
649 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos650 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign651 };
652 self.db
653 .prepare(
654 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
655 VALUES (?, ?, ?, ?, ?, ?)",
656 )
657 .bind(&[
658 row.id.as_str().into(),
659 a.user.id.into(),
660 row.title.as_str().into(),
661 key.public_key.into(),
662 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos663 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign664 ])?
665 .run()
666 .await?;
667 Ok(Outcome::Ok(row.into()))
668 }
669
670 /// Deletes a row the user owns from `table`.
671 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
672 self.db
673 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
674 .bind(&[a.id.into(), a.user.id.into()])?
675 .run()
676 .await?;
677 Ok(())
678 }
679}
680
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas681/// 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 member682/// the last summary's window was still open, so none waits on a later one;
683/// and deleted workspaces past their restore window are purged
684/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas685#[event(scheduled)]
686async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
687 let Ok(db) = env.d1("DB") else { return };
688 let identity = Identity { db, env };
689 if let Err(error) = identity.notify_staff_of_requests().await {
690 worker::console_error!("waitlist summary: {error}");
691 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member692 if let Err(error) = identity.purge_due_workspaces().await {
693 worker::console_error!("workspace purge: {error}");
694 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas695}
696
API and MCP server, Rust identity service, registration, site redesign697#[event(fetch)]
698async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
699 let Some(method) = rpc_method(&request) else {
700 return Response::error("Not found", 404);
701 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents702 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
703 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign704 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents705 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign706
Fast pages, required checks on the branch, self-hosted runners, honest incidents707 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette708 "register" => {
709 let outcome = identity.register(args(body)?).await?;
710 if let Outcome::Ok(signed_in) = &outcome {
711 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
712 }
713 reply(&outcome)
714 }
API and MCP server, Rust identity service, registration, site redesign715 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette716 "create_workspace" => {
717 let outcome = identity.create_workspace(args(body)?).await?;
718 if let Outcome::Ok(workspace) = &outcome {
719 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
720 }
721 reply(&outcome)
722 }
Workspaces own repositories723 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
724 "list_members" => reply(&identity.list_members(args(body)?).await?),
725 "add_member" => reply(&identity.add_member(args(body)?).await?),
726 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette727 "update_workspace" => {
728 let outcome = identity.update_workspace(args(body)?).await?;
729 if let Outcome::Ok(workspace) = &outcome {
730 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
731 }
732 reply(&outcome)
733 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains734 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
735 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
736 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look737 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
738 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
739 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette740 "set_workspace_avatar" => {
741 let outcome = identity.set_workspace_avatar(args(body)?).await?;
742 if let Outcome::Ok(workspace) = &outcome {
743 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
744 }
745 reply(&outcome)
746 }
747 "set_user_avatar" => {
748 let a: SetUserAvatarArgs = args(body)?;
749 let (username, id) = (a.user.username.clone(), a.user.id.clone());
750 let outcome = identity.set_user_avatar(a).await?;
751 if matches!(outcome, Outcome::Ok(_)) {
752 identity.announce_user(&username, Some(&id)).await;
753 }
754 reply(&outcome)
755 }
Agents as a team: lifecycle, merge queue, billing and a new shell756 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
757 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
758 "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 look759 // Signing in with GitHub; see github.rs.
760 "github_enabled" => reply(&identity.github_enabled()),
761 "github_start" => reply(&identity.github_start(args(body)?).await?),
762 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
763 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
764 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
765 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
766 "github_account" => reply(&identity.github_account(args(body)?).await?),
767 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
768 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
769 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
770 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications771 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
772 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
773 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
774 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
775 "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 step776 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API777 "device_start" => reply(&identity.device_start(args(body)?).await?),
778 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
779 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
780 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning781 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
782 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
783 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
784 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 // A person's email addresses; see emails.rs and security.rs.
786 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
787 "add_email" => reply(&identity.add_email(args(body)?).await?),
788 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
789 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
790 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
791 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
792 "security_log" => reply(&identity.security_log(args(body)?).await?),
793 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
794 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
795 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
796 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
797 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign798 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
799 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
800 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
801 "user_for_access_token" => {
802 let a: TokenArgs = args(body)?;
803 reply(&identity.user_for_access_token(&a.token).await?)
804 }
805 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
806 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph807 "usernames" => reply(&identity.usernames(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching808 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains809 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette810 "update_profile" => {
811 let outcome = identity.update_profile(args(body)?).await?;
812 if let Outcome::Ok(profile) = &outcome {
813 identity.announce_user(&profile.username, None).await;
814 }
815 reply(&outcome)
816 }
817 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains818 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign819 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
820 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
821 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
822 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
823 "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 step824 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell825 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
826 "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 API827 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
828 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
829 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign830 "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 look831 // Invites and the waitlist; see invites.rs.
832 "registration" => reply(&identity.registration_mode()),
833 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
834 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
835 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
836 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
837 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
838 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
839 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
840 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
841 "request_access" => reply(&identity.request_access(args(body)?).await?),
842 // Who has access to a repository; see access.rs.
843 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
844 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
845 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
846 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
847 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
848 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
849 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
850 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
851 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'852 // Where a workspace keeps its repositories' git data (EU residency).
853 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
854 "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 look855 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
856 "forget_repo_access" => reply(&identity.forget_repo_access(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace857 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
858 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
859 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
860 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look861 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
862 "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 quotas863 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
865 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
866 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
867 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
868 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
869 "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 member870 // Deleted workspaces, restored or purged by staff; see deletion.rs.
871 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
872 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
873 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign874 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents875 };
876 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign877}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent878
879#[cfg(test)]
880mod register_tests {
881 use super::*;
882
883 #[test]
884 fn nobody_registers_as_g1t() {
885 // What register checks the username with, whatever its case.
886 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
887 assert_eq!(claimable_namespace(username), None, "{username}");
888 }
889 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
890 }
891}

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