g1t/services/identity/src/lib.rs

613 lines21,881 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;
Workspaces own repositories9mod workspaces;
API and MCP server, Rust identity service, registration, site redesign10
11use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos12use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
API and MCP server, Rust identity service, registration, site redesign13use g1t_contracts::{FailureCode, Outcome, User, Viewer, is_valid_namespace, new_id};
14use g1t_kit::{args, now_ms, reply, rpc_method};
15use serde::Deserialize;
16use worker::wasm_bindgen::JsValue;
17use worker::{Context, D1Database, Env, Request, Response, Result, event};
18
RFC 3339 timestamps in identity and repos19const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
20const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
21const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign22const TOKEN_PREFIX: &str = "g1t_";
23const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning24const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
25
26/// A user as selected from the database; `verified` arrives as 0 or 1.
27#[derive(Deserialize)]
28struct Account {
29 id: String,
30 username: String,
31 verified: u8,
32}
33
34impl From<Account> for User {
35 fn from(row: Account) -> Self {
36 User {
37 id: row.id,
38 username: row.username,
39 verified: row.verified != 0,
Workspaces own repositories40 workspaces: Vec::new(),
Email verification, password reset, and Git for AI scale positioning41 }
42 }
43}
API and MCP server, Rust identity service, registration, site redesign44
45#[derive(Deserialize)]
46struct UserRow {
47 id: String,
48 username: String,
49 password_hash: String,
Email verification, password reset, and Git for AI scale positioning50 verified: u8,
API and MCP server, Rust identity service, registration, site redesign51}
52
Email verification, password reset, and Git for AI scale positioning53/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign54#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning55struct TokenOwner {
56 id: String,
57 username: String,
58 email: Option<String>,
59}
60
61#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign62struct KeyRow {
63 id: String,
64 title: String,
65 fingerprint: String,
RFC 3339 timestamps in identity and repos66 created_at: String,
API and MCP server, Rust identity service, registration, site redesign67}
68
69impl From<KeyRow> for SshKey {
70 fn from(row: KeyRow) -> Self {
71 SshKey {
72 id: row.id,
73 title: row.title,
74 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos75 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign76 }
77 }
78}
79
80#[derive(Deserialize)]
81struct TokenRow {
82 id: String,
83 name: String,
RFC 3339 timestamps in identity and repos84 created_at: String,
API and MCP server, Rust identity service, registration, site redesign85}
86
87impl From<TokenRow> for AccessToken {
88 fn from(row: TokenRow) -> Self {
89 AccessToken {
90 id: row.id,
91 name: row.name,
RFC 3339 timestamps in identity and repos92 created_at: row.created_at,
API and MCP server, Rust identity service, registration, site redesign93 }
94 }
95}
96
97struct Identity {
98 db: D1Database,
Email verification, password reset, and Git for AI scale positioning99 env: Env,
API and MCP server, Rust identity service, registration, site redesign100}
101
102impl Identity {
Workspaces own repositories103 /// Runs a query that returns at most one user, for showing to others:
104 /// without their workspaces.
105 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning106 Ok(self
107 .db
API and MCP server, Rust identity service, registration, site redesign108 .prepare(sql)
109 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning110 .first::<Account>(None)
111 .await?
112 .map(User::from))
113 }
114
Workspaces own repositories115 /// Attaches the workspaces a user belongs to, so that any service can
116 /// authorize them without asking again.
117 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
118 let Some(mut user) = user else {
119 return Ok(None);
120 };
121 user.workspaces = self.memberships(&user.id).await?;
122 Ok(Some(user))
123 }
124
125 /// Runs a query that resolves credentials to at most one user.
126 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
127 let user = self.find_public_user(sql, param).await?;
128 self.with_workspaces(user).await
129 }
130
Email verification, password reset, and Git for AI scale positioning131 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos132 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning133 let token = crypto::random_hex(32);
134 self.db
RFC 3339 timestamps in identity and repos135 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning136 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos137 VALUES (?, ?, ?, {})",
138 sql_after(ttl)
139 ))
Email verification, password reset, and Git for AI scale positioning140 .bind(&[
141 crypto::sha256_hex(&token).into(),
142 user_id.into(),
143 kind.into(),
144 ])?
145 .run()
146 .await?;
147 Ok(token)
API and MCP server, Rust identity service, registration, site redesign148 }
149
Email verification, password reset, and Git for AI scale positioning150 /// Consumes a token of `kind`, returning its owner if it was valid.
151 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
152 let id = crypto::sha256_hex(token);
153 let owner = self
154 .db
RFC 3339 timestamps in identity and repos155 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning156 "SELECT users.id, users.username, users.email FROM email_tokens
157 JOIN users ON users.id = email_tokens.user_id
158 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos159 AND email_tokens.expires_at > {SQL_NOW}"
160 ))
Email verification, password reset, and Git for AI scale positioning161 .bind(&[id.as_str().into(), kind.into()])?
162 .first::<TokenOwner>(None)
163 .await?;
164 if let Some(owner) = &owner {
165 // Every outstanding token of this kind dies with the one used.
166 self.db
167 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND kind = ?")
168 .bind(&[owner.id.as_str().into(), kind.into()])?
169 .run()
170 .await?;
171 }
172 Ok(owner)
173 }
174
175 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
176 let token = self
177 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
178 .await?;
179 email::send_verification(&self.env, email, &user.username, &token).await
180 }
181
182 async fn resend_verification(&self, a: UserArgs) -> Result<Outcome<bool>> {
183 let row = self
184 .db
185 .prepare(
186 "SELECT id, username, email FROM users WHERE id = ? AND email_verified_at IS NULL",
187 )
188 .bind(&[a.user.id.as_str().into()])?
189 .first::<TokenOwner>(None)
190 .await?;
191 let Some(TokenOwner {
192 email: Some(email), ..
193 }) = row
194 else {
195 return Ok(Outcome::fail(
196 FailureCode::Conflict,
197 "This account's email is already confirmed.",
198 ));
199 };
200 self.send_verification(&a.user, &email).await?;
201 Ok(Outcome::Ok(true))
202 }
203
204 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
205 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
206 return Ok(Outcome::fail(
207 FailureCode::Invalid,
208 "This confirmation link is not valid or has expired.",
209 ));
210 };
211 self.db
RFC 3339 timestamps in identity and repos212 .prepare(format!(
213 "UPDATE users SET email_verified_at = {SQL_NOW} WHERE id = ?"
214 ))
Email verification, password reset, and Git for AI scale positioning215 .bind(&[owner.id.as_str().into()])?
216 .run()
217 .await?;
218 Ok(Outcome::Ok(User {
219 id: owner.id,
220 username: owner.username,
221 verified: true,
Workspaces own repositories222 ..User::default()
Email verification, password reset, and Git for AI scale positioning223 }))
224 }
225
226 async fn request_password_reset(&self, a: EmailArgs) -> Result<bool> {
227 let row = self
228 .db
229 .prepare("SELECT id, username, email FROM users WHERE email = ?")
230 .bind(&[a.email.trim().to_lowercase().into()])?
231 .first::<TokenOwner>(None)
232 .await?;
233 if let Some(TokenOwner {
234 id,
235 username,
236 email: Some(email),
237 }) = row
238 {
239 let token = self
240 .issue_email_token(&id, "reset", RESET_TTL_SECONDS)
241 .await?;
242 email::send_password_reset(&self.env, &email, &username, &token).await?;
243 }
244 // The same answer either way, so addresses cannot be probed.
245 Ok(true)
246 }
247
248 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
249 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
250 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
251 }
252 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
253 return Ok(Outcome::fail(
254 FailureCode::Invalid,
255 "This reset link is not valid or has expired.",
256 ));
257 };
258 // Following an emailed link also proves the address.
259 self.db
RFC 3339 timestamps in identity and repos260 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning261 "UPDATE users SET password_hash = ?,
RFC 3339 timestamps in identity and repos262 email_verified_at = COALESCE(email_verified_at, {SQL_NOW})
263 WHERE id = ?"
264 ))
Email verification, password reset, and Git for AI scale positioning265 .bind(&[
266 crypto::hash_password(&a.password).into(),
267 owner.id.as_str().into(),
268 ])?
269 .run()
270 .await?;
271 // Anyone signed in with the old password is signed out.
272 self.db
273 .prepare("DELETE FROM sessions WHERE user_id = ?")
274 .bind(&[owner.id.as_str().into()])?
275 .run()
276 .await?;
277 Ok(Outcome::Ok(User {
278 id: owner.id,
279 username: owner.username,
280 verified: true,
Workspaces own repositories281 ..User::default()
Email verification, password reset, and Git for AI scale positioning282 }))
283 }
284
API and MCP server, Rust identity service, registration, site redesign285 async fn user_for_password(&self, username: &str, password: &str) -> Result<Viewer> {
286 let row = self
287 .db
Email verification, password reset, and Git for AI scale positioning288 .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 redesign289 .bind(&[JsValue::from(username.to_lowercase())])?
290 .first::<UserRow>(None)
291 .await?;
Workspaces own repositories292 let user = row
API and MCP server, Rust identity service, registration, site redesign293 .filter(|row| crypto::verify_password(password, &row.password_hash))
294 .map(|row| User {
295 id: row.id,
296 username: row.username,
Email verification, password reset, and Git for AI scale positioning297 verified: row.verified != 0,
Workspaces own repositories298 ..User::default()
299 });
300 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign301 }
302
303 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
304 let username = a.username.trim().to_lowercase();
305 let email = a.email.trim().to_lowercase();
306 let invalid = |message: &str| Ok(Outcome::fail(FailureCode::Invalid, message));
307 if !is_valid_namespace(&username) {
308 return invalid(
309 "Usernames use lowercase letters, digits and single hyphens, up to 39 characters.",
310 );
311 }
312 let well_formed_email = email
313 .split_once('@')
314 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
315 && !email.contains(char::is_whitespace);
316 if !well_formed_email {
317 return invalid("Enter a valid email address.");
318 }
319 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning320 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign321 }
322 let taken = self
323 .db
324 .prepare("SELECT username FROM users WHERE username = ? OR email = ?")
325 .bind(&[username.as_str().into(), email.as_str().into()])?
326 .first::<serde_json::Value>(None)
327 .await?;
328 if taken.is_some() {
329 return Ok(Outcome::fail(
330 FailureCode::Conflict,
331 "That username or email is already registered.",
332 ));
333 }
334 let user = User {
335 id: new_id("usr", now_ms()),
336 username,
Workspaces own repositories337 ..User::default()
API and MCP server, Rust identity service, registration, site redesign338 };
339 self.db
340 .prepare("INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)")
341 .bind(&[
342 user.id.as_str().into(),
343 user.username.as_str().into(),
Email verification, password reset, and Git for AI scale positioning344 email.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign345 crypto::hash_password(&a.password).into(),
346 ])?
347 .run()
348 .await?;
Email verification, password reset, and Git for AI scale positioning349 // The account exists either way; the email can be sent again later.
350 if let Err(error) = self.send_verification(&user, &email).await {
351 worker::console_error!("verification email failed: {error}");
352 }
API and MCP server, Rust identity service, registration, site redesign353 self.start_session(user).await
354 }
355
356 async fn sign_in(&self, a: SignInArgs) -> Result<Outcome<SignedIn>> {
357 let Some(user) = self.user_for_password(&a.username, &a.password).await? else {
358 return Ok(Outcome::fail(
359 FailureCode::Unauthenticated,
360 "Incorrect username or password.",
361 ));
362 };
363 self.start_session(user).await
364 }
365
366 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
367 let session_token = crypto::random_hex(32);
368 self.db
RFC 3339 timestamps in identity and repos369 .prepare(format!(
370 "INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, {})",
371 sql_after(SESSION_TTL_SECONDS)
372 ))
API and MCP server, Rust identity service, registration, site redesign373 .bind(&[
374 crypto::sha256_hex(&session_token).into(),
375 user.id.as_str().into(),
376 ])?
377 .run()
378 .await?;
379 Ok(Outcome::Ok(SignedIn {
380 user,
381 session_token,
382 }))
383 }
384
385 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
386 self.db
387 .prepare("DELETE FROM sessions WHERE id = ?")
388 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
389 .run()
390 .await?;
391 Ok(())
392 }
393
394 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
395 self.find_user(
RFC 3339 timestamps in identity and repos396 &format!(
397 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified
398 FROM sessions JOIN users ON users.id = sessions.user_id
399 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
400 ),
API and MCP server, Rust identity service, registration, site redesign401 &crypto::sha256_hex(&a.session_token),
402 )
403 .await
404 }
405
406 async fn user_for_access_token(&self, token: &str) -> Result<Viewer> {
407 if !token.starts_with(TOKEN_PREFIX) {
408 return Ok(None);
409 }
410 self.find_user(
RFC 3339 timestamps in identity and repos411 &format!(
412 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified
413 FROM access_tokens JOIN users ON users.id = access_tokens.user_id
414 WHERE token_hash = ?
415 AND (access_tokens.expires_at IS NULL OR access_tokens.expires_at > {SQL_NOW})"
416 ),
API and MCP server, Rust identity service, registration, site redesign417 &crypto::sha256_hex(token),
418 )
419 .await
420 }
421
422 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
423 // Like GitHub, a token alone identifies its user.
424 if a.secret.starts_with(TOKEN_PREFIX) {
425 self.user_for_access_token(&a.secret).await
426 } else {
427 self.user_for_password(&a.username, &a.secret).await
428 }
429 }
430
431 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
432 self.find_user(
Email verification, password reset, and Git for AI scale positioning433 "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 redesign434 JOIN users ON users.id = ssh_keys.user_id
435 WHERE fingerprint = ?",
436 &a.fingerprint,
437 )
438 .await
439 }
440
441 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories442 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning443 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign444 &a.username.to_lowercase(),
445 )
446 .await
447 }
448
449 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
450 let rows = self
451 .db
452 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
453 .bind(&[a.user.id.into()])?
454 .all()
455 .await?
456 .results::<KeyRow>()?;
457 Ok(rows.into_iter().map(SshKey::from).collect())
458 }
459
460 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
461 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
462 return Ok(Outcome::fail(
463 FailureCode::Invalid,
464 "That is not a valid OpenSSH public key.",
465 ));
466 };
467 let taken = self
468 .db
469 .prepare("SELECT id FROM ssh_keys WHERE fingerprint = ?")
470 .bind(&[key.fingerprint.as_str().into()])?
471 .first::<serde_json::Value>(None)
472 .await?;
473 if taken.is_some() {
474 return Ok(Outcome::fail(
475 FailureCode::Conflict,
476 "That key is already registered.",
477 ));
478 }
479 let now = now_ms();
480 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
481 .into_iter()
482 .find(|candidate| !candidate.is_empty())
483 .unwrap_or_default()
484 .to_owned();
485 let row = KeyRow {
486 id: new_id("key", now),
487 title,
488 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos489 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign490 };
491 self.db
492 .prepare(
493 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
494 VALUES (?, ?, ?, ?, ?, ?)",
495 )
496 .bind(&[
497 row.id.as_str().into(),
498 a.user.id.into(),
499 row.title.as_str().into(),
500 key.public_key.into(),
501 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos502 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign503 ])?
504 .run()
505 .await?;
506 Ok(Outcome::Ok(row.into()))
507 }
508
509 async fn list_access_tokens(&self, a: UserArgs) -> Result<Vec<AccessToken>> {
510 let rows = self
511 .db
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)512 // Expiring tokens belong to hosted attempts, not to the user's list.
513 .prepare(
514 "SELECT id, name, created_at FROM access_tokens
515 WHERE user_id = ? AND expires_at IS NULL ORDER BY id",
516 )
API and MCP server, Rust identity service, registration, site redesign517 .bind(&[a.user.id.into()])?
518 .all()
519 .await?
520 .results::<TokenRow>()?;
521 Ok(rows.into_iter().map(AccessToken::from).collect())
522 }
523
524 async fn create_access_token(&self, a: CreateAccessTokenArgs) -> Result<CreatedAccessToken> {
525 let token = format!("{TOKEN_PREFIX}{}", crypto::random_hex(20));
526 let now = now_ms();
527 let name = match a.name.trim() {
528 "" => "Access token",
529 name => name,
530 };
531 let row = TokenRow {
532 id: new_id("tok", now),
533 name: name.to_owned(),
RFC 3339 timestamps in identity and repos534 created_at: rfc3339(now),
API and MCP server, Rust identity service, registration, site redesign535 };
536 self.db
537 .prepare(
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)538 "INSERT INTO access_tokens (id, user_id, name, token_hash, created_at, expires_at)
539 VALUES (?, ?, ?, ?, ?, ?)",
API and MCP server, Rust identity service, registration, site redesign540 )
541 .bind(&[
542 row.id.as_str().into(),
543 a.user.id.into(),
544 row.name.as_str().into(),
545 crypto::sha256_hex(&token).into(),
RFC 3339 timestamps in identity and repos546 row.created_at.as_str().into(),
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)547 a.ttl_seconds
RFC 3339 timestamps in identity and repos548 .map_or(JsValue::NULL, |ttl| rfc3339(now + ttl * 1000).into()),
API and MCP server, Rust identity service, registration, site redesign549 ])?
550 .run()
551 .await?;
552 Ok(CreatedAccessToken {
553 token,
554 info: row.into(),
555 })
556 }
557
558 /// Deletes a row the user owns from `table`.
559 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
560 self.db
561 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
562 .bind(&[a.id.into(), a.user.id.into()])?
563 .run()
564 .await?;
565 Ok(())
566 }
567}
568
569#[event(fetch)]
570async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
571 let Some(method) = rpc_method(&request) else {
572 return Response::error("Not found", 404);
573 };
574 let body: serde_json::Value = request.json().await?;
Email verification, password reset, and Git for AI scale positioning575 let identity = Identity {
576 db: env.d1("DB")?,
577 env,
578 };
API and MCP server, Rust identity service, registration, site redesign579
580 match method.as_str() {
581 "register" => reply(&identity.register(args(body)?).await?),
582 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Workspaces own repositories583 "create_workspace" => reply(&identity.create_workspace(args(body)?).await?),
584 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
585 "list_members" => reply(&identity.list_members(args(body)?).await?),
586 "add_member" => reply(&identity.add_member(args(body)?).await?),
587 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API588 "device_start" => reply(&identity.device_start(args(body)?).await?),
589 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
590 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
591 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning592 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
593 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
594 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
595 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign596 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
597 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
598 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
599 "user_for_access_token" => {
600 let a: TokenArgs = args(body)?;
601 reply(&identity.user_for_access_token(&a.token).await?)
602 }
603 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
604 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
605 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
606 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
607 "remove_ssh_key" => reply(&identity.remove("ssh_keys", args(body)?).await?),
608 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
609 "create_access_token" => reply(&identity.create_access_token(args(body)?).await?),
610 "remove_access_token" => reply(&identity.remove("access_tokens", args(body)?).await?),
611 _ => Response::error("Unknown method", 404),
612 }
613}