g1t/services/identity/src/lib.rs

619 lines22,311 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
6mod crypto;
Device sign-in replaces registering and minting tokens over the API7mod device;
Email verification, password reset, and Git for AI scale positioning8mod email;
OAuth 2.1 sign-in for MCP clients and other applications9mod oauth;
Workspaces own repositories10mod workspaces;
API and MCP server, Rust identity service, registration, site redesign11
12use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos13use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
API and MCP server, Rust identity service, registration, site redesign14use g1t_contracts::{FailureCode, Outcome, User, Viewer, is_valid_namespace, new_id};
15use g1t_kit::{args, now_ms, reply, rpc_method};
16use serde::Deserialize;
17use worker::wasm_bindgen::JsValue;
18use worker::{Context, D1Database, Env, Request, Response, Result, event};
19
RFC 3339 timestamps in identity and repos20const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
21const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
22const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign23const TOKEN_PREFIX: &str = "g1t_";
24const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning25const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
26
27/// A user as selected from the database; `verified` arrives as 0 or 1.
28#[derive(Deserialize)]
29struct Account {
30 id: String,
31 username: String,
32 verified: u8,
33}
34
35impl From<Account> for User {
36 fn from(row: Account) -> Self {
37 User {
38 id: row.id,
39 username: row.username,
40 verified: row.verified != 0,
Workspaces own repositories41 workspaces: Vec::new(),
Email verification, password reset, and Git for AI scale positioning42 }
43 }
44}
API and MCP server, Rust identity service, registration, site redesign45
46#[derive(Deserialize)]
47struct UserRow {
48 id: String,
49 username: String,
50 password_hash: String,
Email verification, password reset, and Git for AI scale positioning51 verified: u8,
API and MCP server, Rust identity service, registration, site redesign52}
53
Email verification, password reset, and Git for AI scale positioning54/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign55#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning56struct TokenOwner {
57 id: String,
58 username: String,
59 email: Option<String>,
60}
61
62#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign63struct KeyRow {
64 id: String,
65 title: String,
66 fingerprint: String,
RFC 3339 timestamps in identity and repos67 created_at: String,
API and MCP server, Rust identity service, registration, site redesign68}
69
70impl From<KeyRow> for SshKey {
71 fn from(row: KeyRow) -> Self {
72 SshKey {
73 id: row.id,
74 title: row.title,
75 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos76 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign77 }
78 }
79}
80
81#[derive(Deserialize)]
82struct TokenRow {
83 id: String,
84 name: String,
RFC 3339 timestamps in identity and repos85 created_at: String,
API and MCP server, Rust identity service, registration, site redesign86}
87
88impl From<TokenRow> for AccessToken {
89 fn from(row: TokenRow) -> Self {
90 AccessToken {
91 id: row.id,
92 name: row.name,
RFC 3339 timestamps in identity and repos93 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign94 }
95 }
96}
97
98struct Identity {
99 db: D1Database,
Email verification, password reset, and Git for AI scale positioning100 env: Env,
API and MCP server, Rust identity service, registration, site redesign101}
102
103impl Identity {
Workspaces own repositories104 /// Runs a query that returns at most one user, for showing to others:
105 /// without their workspaces.
106 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning107 Ok(self
108 .db
API and MCP server, Rust identity service, registration, site redesign109 .prepare(sql)
110 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning111 .first::<Account>(None)
112 .await?
113 .map(User::from))
114 }
115
Workspaces own repositories116 /// Attaches the workspaces a user belongs to, so that any service can
117 /// authorize them without asking again.
118 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
119 let Some(mut user) = user else {
120 return Ok(None);
121 };
122 user.workspaces = self.memberships(&user.id).await?;
123 Ok(Some(user))
124 }
125
126 /// Runs a query that resolves credentials to at most one user.
127 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
128 let user = self.find_public_user(sql, param).await?;
129 self.with_workspaces(user).await
130 }
131
Email verification, password reset, and Git for AI scale positioning132 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos133 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning134 let token = crypto::random_hex(32);
135 self.db
RFC 3339 timestamps in identity and repos136 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning137 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos138 VALUES (?, ?, ?, {})",
139 sql_after(ttl)
140 ))
Email verification, password reset, and Git for AI scale positioning141 .bind(&[
142 crypto::sha256_hex(&token).into(),
143 user_id.into(),
144 kind.into(),
145 ])?
146 .run()
147 .await?;
148 Ok(token)
API and MCP server, Rust identity service, registration, site redesign149 }
150
Email verification, password reset, and Git for AI scale positioning151 /// Consumes a token of `kind`, returning its owner if it was valid.
152 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
153 let id = crypto::sha256_hex(token);
154 let owner = self
155 .db
RFC 3339 timestamps in identity and repos156 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning157 "SELECT users.id, users.username, users.email FROM email_tokens
158 JOIN users ON users.id = email_tokens.user_id
159 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos160 AND email_tokens.expires_at > {SQL_NOW}"
161 ))
Email verification, password reset, and Git for AI scale positioning162 .bind(&[id.as_str().into(), kind.into()])?
163 .first::<TokenOwner>(None)
164 .await?;
165 if let Some(owner) = &owner {
166 // Every outstanding token of this kind dies with the one used.
167 self.db
168 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND kind = ?")
169 .bind(&[owner.id.as_str().into(), kind.into()])?
170 .run()
171 .await?;
172 }
173 Ok(owner)
174 }
175
176 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
177 let token = self
178 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
179 .await?;
180 email::send_verification(&self.env, email, &user.username, &token).await
181 }
182
183 async fn resend_verification(&self, a: UserArgs) -> Result<Outcome<bool>> {
184 let row = self
185 .db
186 .prepare(
187 "SELECT id, username, email FROM users WHERE id = ? AND email_verified_at IS NULL",
188 )
189 .bind(&[a.user.id.as_str().into()])?
190 .first::<TokenOwner>(None)
191 .await?;
192 let Some(TokenOwner {
193 email: Some(email), ..
194 }) = row
195 else {
196 return Ok(Outcome::fail(
197 FailureCode::Conflict,
198 "This account's email is already confirmed.",
199 ));
200 };
201 self.send_verification(&a.user, &email).await?;
202 Ok(Outcome::Ok(true))
203 }
204
205 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
206 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
207 return Ok(Outcome::fail(
208 FailureCode::Invalid,
209 "This confirmation link is not valid or has expired.",
210 ));
211 };
212 self.db
RFC 3339 timestamps in identity and repos213 .prepare(format!(
214 "UPDATE users SET email_verified_at = {SQL_NOW} WHERE id = ?"
215 ))
Email verification, password reset, and Git for AI scale positioning216 .bind(&[owner.id.as_str().into()])?
217 .run()
218 .await?;
219 Ok(Outcome::Ok(User {
220 id: owner.id,
221 username: owner.username,
222 verified: true,
Workspaces own repositories223 ..User::default()
Email verification, password reset, and Git for AI scale positioning224 }))
225 }
226
227 async fn request_password_reset(&self, a: EmailArgs) -> Result<bool> {
228 let row = self
229 .db
230 .prepare("SELECT id, username, email FROM users WHERE email = ?")
231 .bind(&[a.email.trim().to_lowercase().into()])?
232 .first::<TokenOwner>(None)
233 .await?;
234 if let Some(TokenOwner {
235 id,
236 username,
237 email: Some(email),
238 }) = row
239 {
240 let token = self
241 .issue_email_token(&id, "reset", RESET_TTL_SECONDS)
242 .await?;
243 email::send_password_reset(&self.env, &email, &username, &token).await?;
244 }
245 // The same answer either way, so addresses cannot be probed.
246 Ok(true)
247 }
248
249 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
250 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
251 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
252 }
253 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
254 return Ok(Outcome::fail(
255 FailureCode::Invalid,
256 "This reset link is not valid or has expired.",
257 ));
258 };
259 // Following an emailed link also proves the address.
260 self.db
RFC 3339 timestamps in identity and repos261 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning262 "UPDATE users SET password_hash = ?,
RFC 3339 timestamps in identity and repos263 email_verified_at = COALESCE(email_verified_at, {SQL_NOW})
264 WHERE id = ?"
265 ))
Email verification, password reset, and Git for AI scale positioning266 .bind(&[
267 crypto::hash_password(&a.password).into(),
268 owner.id.as_str().into(),
269 ])?
270 .run()
271 .await?;
272 // Anyone signed in with the old password is signed out.
273 self.db
274 .prepare("DELETE FROM sessions WHERE user_id = ?")
275 .bind(&[owner.id.as_str().into()])?
276 .run()
277 .await?;
278 Ok(Outcome::Ok(User {
279 id: owner.id,
280 username: owner.username,
281 verified: true,
Workspaces own repositories282 ..User::default()
Email verification, password reset, and Git for AI scale positioning283 }))
284 }
285
API and MCP server, Rust identity service, registration, site redesign286 async fn user_for_password(&self, username: &str, password: &str) -> Result<Viewer> {
287 let row = self
288 .db
Email verification, password reset, and Git for AI scale positioning289 .prepare("SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?")
API and MCP server, Rust identity service, registration, site redesign290 .bind(&[JsValue::from(username.to_lowercase())])?
291 .first::<UserRow>(None)
292 .await?;
Workspaces own repositories293 let user = row
API and MCP server, Rust identity service, registration, site redesign294 .filter(|row| crypto::verify_password(password, &row.password_hash))
295 .map(|row| User {
296 id: row.id,
297 username: row.username,
Email verification, password reset, and Git for AI scale positioning298 verified: row.verified != 0,
Workspaces own repositories299 ..User::default()
300 });
301 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign302 }
303
304 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
305 let username = a.username.trim().to_lowercase();
306 let email = a.email.trim().to_lowercase();
307 let invalid = |message: &str| Ok(Outcome::fail(FailureCode::Invalid, message));
308 if !is_valid_namespace(&username) {
309 return invalid(
310 "Usernames use lowercase letters, digits and single hyphens, up to 39 characters.",
311 );
312 }
313 let well_formed_email = email
314 .split_once('@')
315 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
316 && !email.contains(char::is_whitespace);
317 if !well_formed_email {
318 return invalid("Enter a valid email address.");
319 }
320 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning321 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign322 }
323 let taken = self
324 .db
325 .prepare("SELECT username FROM users WHERE username = ? OR email = ?")
326 .bind(&[username.as_str().into(), email.as_str().into()])?
327 .first::<serde_json::Value>(None)
328 .await?;
329 if taken.is_some() {
330 return Ok(Outcome::fail(
331 FailureCode::Conflict,
332 "That username or email is already registered.",
333 ));
334 }
335 let user = User {
336 id: new_id("usr", now_ms()),
337 username,
Workspaces own repositories338 ..User::default()
API and MCP server, Rust identity service, registration, site redesign339 };
340 self.db
341 .prepare("INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)")
342 .bind(&[
343 user.id.as_str().into(),
344 user.username.as_str().into(),
Email verification, password reset, and Git for AI scale positioning345 email.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign346 crypto::hash_password(&a.password).into(),
347 ])?
348 .run()
349 .await?;
Email verification, password reset, and Git for AI scale positioning350 // The account exists either way; the email can be sent again later.
351 if let Err(error) = self.send_verification(&user, &email).await {
352 worker::console_error!("verification email failed: {error}");
353 }
API and MCP server, Rust identity service, registration, site redesign354 self.start_session(user).await
355 }
356
357 async fn sign_in(&self, a: SignInArgs) -> Result<Outcome<SignedIn>> {
358 let Some(user) = self.user_for_password(&a.username, &a.password).await? else {
359 return Ok(Outcome::fail(
360 FailureCode::Unauthenticated,
361 "Incorrect username or password.",
362 ));
363 };
364 self.start_session(user).await
365 }
366
367 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
368 let session_token = crypto::random_hex(32);
369 self.db
RFC 3339 timestamps in identity and repos370 .prepare(format!(
371 "INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, {})",
372 sql_after(SESSION_TTL_SECONDS)
373 ))
API and MCP server, Rust identity service, registration, site redesign374 .bind(&[
375 crypto::sha256_hex(&session_token).into(),
376 user.id.as_str().into(),
377 ])?
378 .run()
379 .await?;
380 Ok(Outcome::Ok(SignedIn {
381 user,
382 session_token,
383 }))
384 }
385
386 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
387 self.db
388 .prepare("DELETE FROM sessions WHERE id = ?")
389 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
390 .run()
391 .await?;
392 Ok(())
393 }
394
395 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
396 self.find_user(
RFC 3339 timestamps in identity and repos397 &format!(
398 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified
399 FROM sessions JOIN users ON users.id = sessions.user_id
400 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
401 ),
API and MCP server, Rust identity service, registration, site redesign402 &crypto::sha256_hex(&a.session_token),
403 )
404 .await
405 }
406
407 async fn user_for_access_token(&self, token: &str) -> Result<Viewer> {
408 if !token.starts_with(TOKEN_PREFIX) {
409 return Ok(None);
410 }
411 self.find_user(
RFC 3339 timestamps in identity and repos412 &format!(
413 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified
414 FROM access_tokens JOIN users ON users.id = access_tokens.user_id
415 WHERE token_hash = ?
416 AND (access_tokens.expires_at IS NULL OR access_tokens.expires_at > {SQL_NOW})"
417 ),
API and MCP server, Rust identity service, registration, site redesign418 &crypto::sha256_hex(token),
419 )
420 .await
421 }
422
423 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
424 // Like GitHub, a token alone identifies its user.
425 if a.secret.starts_with(TOKEN_PREFIX) {
426 self.user_for_access_token(&a.secret).await
427 } else {
428 self.user_for_password(&a.username, &a.secret).await
429 }
430 }
431
432 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
433 self.find_user(
Email verification, password reset, and Git for AI scale positioning434 "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 redesign435 JOIN users ON users.id = ssh_keys.user_id
436 WHERE fingerprint = ?",
437 &a.fingerprint,
438 )
439 .await
440 }
441
442 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories443 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning444 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign445 &a.username.to_lowercase(),
446 )
447 .await
448 }
449
450 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
451 let rows = self
452 .db
453 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
454 .bind(&[a.user.id.into()])?
455 .all()
456 .await?
457 .results::<KeyRow>()?;
458 Ok(rows.into_iter().map(SshKey::from).collect())
459 }
460
461 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
462 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
463 return Ok(Outcome::fail(
464 FailureCode::Invalid,
465 "That is not a valid OpenSSH public key.",
466 ));
467 };
468 let taken = self
469 .db
470 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
471 .bind(&[key.fingerprint.as_str().into()])?
472 .first::<serde_json::Value>(None)
473 .await?;
474 if taken.is_some() {
475 return Ok(Outcome::fail(
476 FailureCode::Conflict,
477 "That key is already registered.",
478 ));
479 }
480 let now = now_ms();
481 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
482 .into_iter()
483 .find(|candidate| !candidate.is_empty())
484 .unwrap_or_default()
485 .to_owned();
486 let row = KeyRow {
487 id: new_id("key", now),
488 title,
489 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos490 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign491 };
492 self.db
493 .prepare(
494 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
495 VALUES (?, ?, ?, ?, ?, ?)",
496 )
497 .bind(&[
498 row.id.as_str().into(),
499 a.user.id.into(),
500 row.title.as_str().into(),
501 key.public_key.into(),
502 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos503 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign504 ])?
505 .run()
506 .await?;
507 Ok(Outcome::Ok(row.into()))
508 }
509
510 async fn list_access_tokens(&self, a: UserArgs) -> Result<Vec<AccessToken>> {
511 let rows = self
512 .db
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)513 // Expiring tokens belong to hosted attempts, not to the user's list.
514 .prepare(
515 "SELECT id, name, created_at FROM access_tokens
516 WHERE user_id = ? AND expires_at IS NULL ORDER BY id",
517 )
API and MCP server, Rust identity service, registration, site redesign518 .bind(&[a.user.id.into()])?
519 .all()
520 .await?
521 .results::<TokenRow>()?;
522 Ok(rows.into_iter().map(AccessToken::from).collect())
523 }
524
525 async fn create_access_token(&self, a: CreateAccessTokenArgs) -> Result<CreatedAccessToken> {
526 let token = format!("{TOKEN_PREFIX}{}", crypto::random_hex(20));
527 let now = now_ms();
528 let name = match a.name.trim() {
529 "" => "Access token",
530 name => name,
531 };
532 let row = TokenRow {
533 id: new_id("tok", now),
534 name: name.to_owned(),
RFC 3339 timestamps in identity and repos535 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign536 };
537 self.db
538 .prepare(
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)539 "INSERT INTO access_tokens (id, user_id, name, token_hash, created_at, expires_at)
540 VALUES (?, ?, ?, ?, ?, ?)",
API and MCP server, Rust identity service, registration, site redesign541 )
542 .bind(&[
543 row.id.as_str().into(),
544 a.user.id.into(),
545 row.name.as_str().into(),
546 crypto::sha256_hex(&token).into(),
RFC 3339 timestamps in identity and repos547 row.created_at.as_str().into(),
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)548 a.ttl_seconds
RFC 3339 timestamps in identity and repos549 .map_or(JsValue::NULL, |ttl| rfc3339(now + ttl * 1000).into()),
API and MCP server, Rust identity service, registration, site redesign550 ])?
551 .run()
552 .await?;
553 Ok(CreatedAccessToken {
554 token,
555 info: row.into(),
556 })
557 }
558
559 /// Deletes a row the user owns from `table`.
560 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
561 self.db
562 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
563 .bind(&[a.id.into(), a.user.id.into()])?
564 .run()
565 .await?;
566 Ok(())
567 }
568}
569
570#[event(fetch)]
571async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
572 let Some(method) = rpc_method(&request) else {
573 return Response::error("Not found", 404);
574 };
575 let body: serde_json::Value = request.json().await?;
Email verification, password reset, and Git for AI scale positioning576 let identity = Identity {
577 db: env.d1("DB")?,
578 env,
579 };
API and MCP server, Rust identity service, registration, site redesign580
581 match method.as_str() {
582 "register" => reply(&identity.register(args(body)?).await?),
583 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Workspaces own repositories584 "create_workspace" => reply(&identity.create_workspace(args(body)?).await?),
585 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
586 "list_members" => reply(&identity.list_members(args(body)?).await?),
587 "add_member" => reply(&identity.add_member(args(body)?).await?),
588 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications589 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
590 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
591 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
592 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
593 "revoke_oauth_grant" => reply(&identity.revoke_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API594 "device_start" => reply(&identity.device_start(args(body)?).await?),
595 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
596 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
597 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning598 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
599 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
600 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
601 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign602 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
603 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
604 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
605 "user_for_access_token" => {
606 let a: TokenArgs = args(body)?;
607 reply(&identity.user_for_access_token(&a.token).await?)
608 }
609 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
610 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
611 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
612 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
613 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
614 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
615 "create_access_token" => reply(&identity.create_access_token(args(body)?).await?),
616 "remove_access_token" => reply(&identity.remove("access_tokens", args(body)?).await?),
617 _ => Response::error("Unknown method", 404),
618 }
619}