this repo has no description
1use crate::api::{ApiError, DidResponse, EmptyResponse}; 2use crate::auth::BearerAuthAllowDeactivated; 3use crate::plc::signing_key_to_did_key; 4use crate::state::AppState; 5use axum::{ 6 Json, 7 extract::{Path, Query, State}, 8 http::{HeaderMap, StatusCode}, 9 response::{IntoResponse, Response}, 10}; 11use base64::Engine; 12use k256::SecretKey; 13use k256::elliptic_curve::sec1::ToEncodedPoint; 14use serde::{Deserialize, Serialize}; 15use serde_json::json; 16use tracing::{error, warn}; 17 18#[derive(Debug, Clone, Serialize, Deserialize)] 19#[serde(rename_all = "camelCase")] 20pub struct DidWebVerificationMethod { 21 pub id: String, 22 #[serde(rename = "type")] 23 pub method_type: String, 24 pub public_key_multibase: String, 25} 26 27#[derive(Deserialize)] 28pub struct ResolveHandleParams { 29 pub handle: String, 30} 31 32pub async fn resolve_handle( 33 State(state): State<AppState>, 34 Query(params): Query<ResolveHandleParams>, 35) -> Response { 36 let handle = params.handle.trim(); 37 if handle.is_empty() { 38 return ApiError::InvalidRequest("handle is required".into()).into_response(); 39 } 40 let cache_key = format!("handle:{}", handle); 41 if let Some(did) = state.cache.get(&cache_key).await { 42 return DidResponse::response(did).into_response(); 43 } 44 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle) 45 .fetch_optional(&state.db) 46 .await; 47 match user { 48 Ok(Some(row)) => { 49 let _ = state 50 .cache 51 .set(&cache_key, &row.did, std::time::Duration::from_secs(300)) 52 .await; 53 DidResponse::response(row.did).into_response() 54 } 55 Ok(None) => match crate::handle::resolve_handle(handle).await { 56 Ok(did) => { 57 let _ = state 58 .cache 59 .set(&cache_key, &did, std::time::Duration::from_secs(300)) 60 .await; 61 DidResponse::response(did).into_response() 62 } 63 Err(_) => ApiError::HandleNotFound.into_response(), 64 }, 65 Err(e) => { 66 error!("DB error resolving handle: {:?}", e); 67 ApiError::InternalError(None).into_response() 68 } 69 } 70} 71 72pub fn get_jwk(key_bytes: &[u8]) -> Result<serde_json::Value, &'static str> { 73 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| "Invalid key length")?; 74 let public_key = secret_key.public_key(); 75 let encoded = public_key.to_encoded_point(false); 76 let x = encoded.x().ok_or("Missing x coordinate")?; 77 let y = encoded.y().ok_or("Missing y coordinate")?; 78 let x_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x); 79 let y_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y); 80 Ok(json!({ 81 "kty": "EC", 82 "crv": "secp256k1", 83 "x": x_b64, 84 "y": y_b64 85 })) 86} 87 88pub fn get_public_key_multibase(key_bytes: &[u8]) -> Result<String, &'static str> { 89 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| "Invalid key length")?; 90 let public_key = secret_key.public_key(); 91 let compressed = public_key.to_encoded_point(true); 92 let compressed_bytes = compressed.as_bytes(); 93 let mut multicodec_key = vec![0xe7, 0x01]; 94 multicodec_key.extend_from_slice(compressed_bytes); 95 Ok(format!("z{}", bs58::encode(&multicodec_key).into_string())) 96} 97 98pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 99 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 100 let host_header = headers 101 .get("host") 102 .and_then(|h| h.to_str().ok()) 103 .unwrap_or(&hostname); 104 let host_without_port = host_header.split(':').next().unwrap_or(host_header); 105 let hostname_without_port = hostname.split(':').next().unwrap_or(&hostname); 106 if host_without_port != hostname_without_port 107 && host_without_port.ends_with(&format!(".{}", hostname_without_port)) 108 { 109 let handle = host_without_port 110 .strip_suffix(&format!(".{}", hostname_without_port)) 111 .unwrap_or(host_without_port); 112 return serve_subdomain_did_doc(&state, handle, &hostname).await; 113 } 114 let did = if hostname.contains(':') { 115 format!("did:web:{}", hostname.replace(':', "%3A")) 116 } else { 117 format!("did:web:{}", hostname) 118 }; 119 Json(json!({ 120 "@context": ["https://www.w3.org/ns/did/v1"], 121 "id": did, 122 "service": [{ 123 "id": "#atproto_pds", 124 "type": "AtprotoPersonalDataServer", 125 "serviceEndpoint": format!("https://{}", hostname) 126 }] 127 })) 128 .into_response() 129} 130 131async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response { 132 let full_handle = format!("{}.{}", handle, hostname); 133 let user = sqlx::query!( 134 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 135 full_handle 136 ) 137 .fetch_optional(&state.db) 138 .await; 139 let (user_id, did, migrated_to_pds) = match user { 140 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 141 Ok(None) => { 142 return ApiError::NotFoundMsg("User not found".into()).into_response(); 143 } 144 Err(e) => { 145 error!("DB Error: {:?}", e); 146 return ApiError::InternalError(None).into_response(); 147 } 148 }; 149 if !did.starts_with("did:web:") { 150 return ApiError::NotFoundMsg("User is not did:web".into()).into_response(); 151 } 152 let subdomain_host = format!("{}.{}", handle, hostname); 153 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 154 let expected_self_hosted = format!("did:web:{}", encoded_subdomain); 155 if did != expected_self_hosted { 156 return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into()) 157 .into_response(); 158 } 159 160 let overrides = sqlx::query!( 161 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 162 user_id 163 ) 164 .fetch_optional(&state.db) 165 .await 166 .ok() 167 .flatten(); 168 169 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 170 171 if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { 172 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 173 .ok() 174 .filter(|p| !p.is_empty()) 175 .map(|p| (ovr, p)) 176 }) { 177 let also_known_as = if !ovr.also_known_as.is_empty() { 178 ovr.also_known_as.clone() 179 } else { 180 vec![format!("at://{}", full_handle)] 181 }; 182 183 return Json(json!({ 184 "@context": [ 185 "https://www.w3.org/ns/did/v1", 186 "https://w3id.org/security/multikey/v1", 187 "https://w3id.org/security/suites/secp256k1-2019/v1" 188 ], 189 "id": did, 190 "alsoKnownAs": also_known_as, 191 "verificationMethod": parsed.iter().map(|m| json!({ 192 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 193 "type": m.method_type, 194 "controller": did, 195 "publicKeyMultibase": m.public_key_multibase 196 })).collect::<Vec<_>>(), 197 "service": [{ 198 "id": "#atproto_pds", 199 "type": "AtprotoPersonalDataServer", 200 "serviceEndpoint": service_endpoint 201 }] 202 })) 203 .into_response(); 204 } 205 206 let key_row = sqlx::query!( 207 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 208 user_id 209 ) 210 .fetch_optional(&state.db) 211 .await; 212 let key_bytes: Vec<u8> = match key_row { 213 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 214 Ok(k) => k, 215 Err(_) => { 216 return ApiError::InternalError(None).into_response(); 217 } 218 }, 219 _ => { 220 return ApiError::InternalError(None).into_response(); 221 } 222 }; 223 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 224 Ok(pk) => pk, 225 Err(e) => { 226 tracing::error!("Failed to generate public key multibase: {}", e); 227 return ApiError::InternalError(None).into_response(); 228 } 229 }; 230 231 let also_known_as = if let Some(ref ovr) = overrides { 232 if !ovr.also_known_as.is_empty() { 233 ovr.also_known_as.clone() 234 } else { 235 vec![format!("at://{}", full_handle)] 236 } 237 } else { 238 vec![format!("at://{}", full_handle)] 239 }; 240 241 Json(json!({ 242 "@context": [ 243 "https://www.w3.org/ns/did/v1", 244 "https://w3id.org/security/multikey/v1", 245 "https://w3id.org/security/suites/secp256k1-2019/v1" 246 ], 247 "id": did, 248 "alsoKnownAs": also_known_as, 249 "verificationMethod": [{ 250 "id": format!("{}#atproto", did), 251 "type": "Multikey", 252 "controller": did, 253 "publicKeyMultibase": public_key_multibase 254 }], 255 "service": [{ 256 "id": "#atproto_pds", 257 "type": "AtprotoPersonalDataServer", 258 "serviceEndpoint": service_endpoint 259 }] 260 })) 261 .into_response() 262} 263 264pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 265 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 266 let full_handle = format!("{}.{}", handle, hostname); 267 let user = sqlx::query!( 268 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 269 full_handle 270 ) 271 .fetch_optional(&state.db) 272 .await; 273 let (user_id, did, migrated_to_pds) = match user { 274 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 275 Ok(None) => { 276 return ApiError::NotFoundMsg("User not found".into()).into_response(); 277 } 278 Err(e) => { 279 error!("DB Error: {:?}", e); 280 return ApiError::InternalError(None).into_response(); 281 } 282 }; 283 if !did.starts_with("did:web:") { 284 return ApiError::NotFoundMsg("User is not did:web".into()).into_response(); 285 } 286 let encoded_hostname = hostname.replace(':', "%3A"); 287 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle); 288 let subdomain_host = format!("{}.{}", handle, hostname); 289 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 290 let new_subdomain_format = format!("did:web:{}", encoded_subdomain); 291 if did != old_path_format && did != new_subdomain_format { 292 return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into()) 293 .into_response(); 294 } 295 296 let overrides = sqlx::query!( 297 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 298 user_id 299 ) 300 .fetch_optional(&state.db) 301 .await 302 .ok() 303 .flatten(); 304 305 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 306 307 if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { 308 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 309 .ok() 310 .filter(|p| !p.is_empty()) 311 .map(|p| (ovr, p)) 312 }) { 313 let also_known_as = if !ovr.also_known_as.is_empty() { 314 ovr.also_known_as.clone() 315 } else { 316 vec![format!("at://{}", full_handle)] 317 }; 318 319 return Json(json!({ 320 "@context": [ 321 "https://www.w3.org/ns/did/v1", 322 "https://w3id.org/security/multikey/v1", 323 "https://w3id.org/security/suites/secp256k1-2019/v1" 324 ], 325 "id": did, 326 "alsoKnownAs": also_known_as, 327 "verificationMethod": parsed.iter().map(|m| json!({ 328 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 329 "type": m.method_type, 330 "controller": did, 331 "publicKeyMultibase": m.public_key_multibase 332 })).collect::<Vec<_>>(), 333 "service": [{ 334 "id": "#atproto_pds", 335 "type": "AtprotoPersonalDataServer", 336 "serviceEndpoint": service_endpoint 337 }] 338 })) 339 .into_response(); 340 } 341 342 let key_row = sqlx::query!( 343 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 344 user_id 345 ) 346 .fetch_optional(&state.db) 347 .await; 348 let key_bytes: Vec<u8> = match key_row { 349 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 350 Ok(k) => k, 351 Err(_) => { 352 return ApiError::InternalError(None).into_response(); 353 } 354 }, 355 _ => { 356 return ApiError::InternalError(None).into_response(); 357 } 358 }; 359 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 360 Ok(pk) => pk, 361 Err(e) => { 362 tracing::error!("Failed to generate public key multibase: {}", e); 363 return ApiError::InternalError(None).into_response(); 364 } 365 }; 366 367 let also_known_as = if let Some(ref ovr) = overrides { 368 if !ovr.also_known_as.is_empty() { 369 ovr.also_known_as.clone() 370 } else { 371 vec![format!("at://{}", full_handle)] 372 } 373 } else { 374 vec![format!("at://{}", full_handle)] 375 }; 376 377 Json(json!({ 378 "@context": [ 379 "https://www.w3.org/ns/did/v1", 380 "https://w3id.org/security/multikey/v1", 381 "https://w3id.org/security/suites/secp256k1-2019/v1" 382 ], 383 "id": did, 384 "alsoKnownAs": also_known_as, 385 "verificationMethod": [{ 386 "id": format!("{}#atproto", did), 387 "type": "Multikey", 388 "controller": did, 389 "publicKeyMultibase": public_key_multibase 390 }], 391 "service": [{ 392 "id": "#atproto_pds", 393 "type": "AtprotoPersonalDataServer", 394 "serviceEndpoint": service_endpoint 395 }] 396 })) 397 .into_response() 398} 399 400pub async fn verify_did_web( 401 did: &str, 402 hostname: &str, 403 handle: &str, 404 expected_signing_key: Option<&str>, 405) -> Result<(), String> { 406 let subdomain_host = format!("{}.{}", handle, hostname); 407 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 408 let expected_subdomain_did = format!("did:web:{}", encoded_subdomain); 409 if did == expected_subdomain_did { 410 return Ok(()); 411 } 412 let expected_prefix = if hostname.contains(':') { 413 format!("did:web:{}", hostname.replace(':', "%3A")) 414 } else { 415 format!("did:web:{}", hostname) 416 }; 417 if did.starts_with(&expected_prefix) { 418 let suffix = &did[expected_prefix.len()..]; 419 let expected_suffix = format!(":u:{}", handle); 420 if suffix == expected_suffix { 421 return Ok(()); 422 } else { 423 return Err(format!( 424 "Invalid DID path for this PDS. Expected {}", 425 expected_suffix 426 )); 427 } 428 } 429 let expected_signing_key = expected_signing_key.ok_or_else(|| { 430 "External did:web requires a pre-reserved signing key. Call com.atproto.server.reserveSigningKey first, configure your DID document with the returned key, then provide the signingKey in createAccount.".to_string() 431 })?; 432 let parts: Vec<&str> = did.split(':').collect(); 433 if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 434 return Err("Invalid did:web format".into()); 435 } 436 let domain_segment = parts[2]; 437 let domain = domain_segment.replace("%3A", ":"); 438 let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 439 "http" 440 } else { 441 "https" 442 }; 443 let url = if parts.len() == 3 { 444 format!("{}://{}/.well-known/did.json", scheme, domain) 445 } else { 446 let path = parts[3..].join("/"); 447 format!("{}://{}/{}/did.json", scheme, domain, path) 448 }; 449 let client = crate::api::proxy_client::did_resolution_client(); 450 let resp = client 451 .get(&url) 452 .send() 453 .await 454 .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 455 if !resp.status().is_success() { 456 return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 457 } 458 let doc: serde_json::Value = resp 459 .json() 460 .await 461 .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 462 let services = doc["service"] 463 .as_array() 464 .ok_or("No services found in DID doc")?; 465 let pds_endpoint = format!("https://{}", hostname); 466 let has_valid_service = services 467 .iter() 468 .any(|s| s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint); 469 if !has_valid_service { 470 return Err(format!( 471 "DID document does not list this PDS ({}) as AtprotoPersonalDataServer", 472 pds_endpoint 473 )); 474 } 475 let verification_methods = doc["verificationMethod"] 476 .as_array() 477 .ok_or("No verificationMethod found in DID doc")?; 478 let expected_multibase = expected_signing_key 479 .strip_prefix("did:key:") 480 .ok_or("Invalid signing key format")?; 481 let has_matching_key = verification_methods.iter().any(|vm| { 482 vm["publicKeyMultibase"] 483 .as_str() 484 .map(|pk| pk == expected_multibase) 485 .unwrap_or(false) 486 }); 487 if !has_matching_key { 488 return Err(format!( 489 "DID document verification key does not match reserved signing key. Expected publicKeyMultibase: {}", 490 expected_multibase 491 )); 492 } 493 Ok(()) 494} 495 496#[derive(serde::Serialize)] 497#[serde(rename_all = "camelCase")] 498pub struct GetRecommendedDidCredentialsOutput { 499 pub rotation_keys: Vec<String>, 500 pub also_known_as: Vec<String>, 501 pub verification_methods: VerificationMethods, 502 pub services: Services, 503} 504 505#[derive(serde::Serialize)] 506#[serde(rename_all = "camelCase")] 507pub struct VerificationMethods { 508 pub atproto: String, 509} 510 511#[derive(serde::Serialize)] 512pub struct Services { 513 pub atproto_pds: AtprotoPds, 514} 515 516#[derive(serde::Serialize)] 517#[serde(rename_all = "camelCase")] 518pub struct AtprotoPds { 519 #[serde(rename = "type")] 520 pub service_type: String, 521 pub endpoint: String, 522} 523 524pub async fn get_recommended_did_credentials( 525 State(state): State<AppState>, 526 auth: BearerAuthAllowDeactivated, 527) -> Response { 528 let auth_user = auth.0; 529 let user = match sqlx::query!( 530 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 531 &auth_user.did 532 ) 533 .fetch_optional(&state.db) 534 .await 535 { 536 Ok(Some(row)) => row, 537 _ => return ApiError::InternalError(None).into_response(), 538 }; 539 let key_bytes = match auth_user.key_bytes { 540 Some(kb) => kb, 541 None => { 542 return ApiError::AuthenticationFailed(Some( 543 "OAuth tokens cannot get DID credentials".into(), 544 )) 545 .into_response(); 546 } 547 }; 548 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 549 let pds_endpoint = format!("https://{}", hostname); 550 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 551 Ok(k) => k, 552 Err(_) => return ApiError::InternalError(None).into_response(), 553 }; 554 let did_key = signing_key_to_did_key(&signing_key); 555 let rotation_keys = if auth_user.did.starts_with("did:web:") { 556 vec![] 557 } else { 558 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { 559 Ok(key) => key, 560 Err(_) => { 561 warn!( 562 "PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation" 563 ); 564 did_key.clone() 565 } 566 }; 567 vec![server_rotation_key] 568 }; 569 ( 570 StatusCode::OK, 571 Json(GetRecommendedDidCredentialsOutput { 572 rotation_keys, 573 also_known_as: vec![format!("at://{}", user.handle)], 574 verification_methods: VerificationMethods { atproto: did_key }, 575 services: Services { 576 atproto_pds: AtprotoPds { 577 service_type: "AtprotoPersonalDataServer".to_string(), 578 endpoint: pds_endpoint, 579 }, 580 }, 581 }), 582 ) 583 .into_response() 584} 585 586#[derive(Deserialize)] 587pub struct UpdateHandleInput { 588 pub handle: String, 589} 590 591pub async fn update_handle( 592 State(state): State<AppState>, 593 auth: BearerAuthAllowDeactivated, 594 Json(input): Json<UpdateHandleInput>, 595) -> Response { 596 let auth_user = auth.0; 597 if let Err(e) = crate::auth::scope_check::check_identity_scope( 598 auth_user.is_oauth, 599 auth_user.scope.as_deref(), 600 crate::oauth::scopes::IdentityAttr::Handle, 601 ) { 602 return e; 603 } 604 let did = auth_user.did; 605 if !state 606 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 607 .await 608 { 609 return ApiError::RateLimitExceeded(Some( 610 "Too many handle updates. Try again later.".into(), 611 )) 612 .into_response(); 613 } 614 if !state 615 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 616 .await 617 { 618 return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 619 .into_response(); 620 } 621 let user_row = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str()) 622 .fetch_optional(&state.db) 623 .await 624 { 625 Ok(Some(row)) => row, 626 _ => return ApiError::InternalError(None).into_response(), 627 }; 628 let user_id = user_row.id; 629 let current_handle = user_row.handle; 630 let new_handle = input.handle.trim().to_ascii_lowercase(); 631 if new_handle.is_empty() { 632 return ApiError::InvalidRequest("handle is required".into()).into_response(); 633 } 634 if !new_handle 635 .chars() 636 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 637 { 638 return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 639 .into_response(); 640 } 641 for segment in new_handle.split('.') { 642 if segment.is_empty() { 643 return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 644 .into_response(); 645 } 646 if segment.starts_with('-') || segment.ends_with('-') { 647 return ApiError::InvalidHandle(Some( 648 "Handle segment cannot start or end with hyphen".into(), 649 )) 650 .into_response(); 651 } 652 } 653 if crate::moderation::has_explicit_slur(&new_handle) { 654 return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 655 .into_response(); 656 } 657 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 658 let suffix = format!(".{}", hostname); 659 let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname); 660 let handle = if is_service_domain { 661 let short_part = if new_handle.ends_with(&suffix) { 662 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 663 } else { 664 &new_handle 665 }; 666 let full_handle = if new_handle.ends_with(&suffix) { 667 new_handle.clone() 668 } else { 669 format!("{}.{}", new_handle, hostname) 670 }; 671 if full_handle == current_handle { 672 if let Err(e) = 673 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)) 674 .await 675 { 676 warn!("Failed to sequence identity event for handle update: {}", e); 677 } 678 return EmptyResponse::ok().into_response(); 679 } 680 if short_part.contains('.') { 681 return ApiError::InvalidHandle(Some( 682 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 683 )) 684 .into_response(); 685 } 686 if short_part.len() < 3 { 687 return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 688 } 689 if short_part.len() > 18 { 690 return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 691 } 692 full_handle 693 } else { 694 if new_handle == current_handle { 695 if let Err(e) = 696 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle)) 697 .await 698 { 699 warn!("Failed to sequence identity event for handle update: {}", e); 700 } 701 return EmptyResponse::ok().into_response(); 702 } 703 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 704 Ok(()) => {} 705 Err(crate::handle::HandleResolutionError::NotFound) => { 706 return ApiError::HandleNotAvailable(None).into_response(); 707 } 708 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 709 return ApiError::HandleNotAvailable(Some(format!( 710 "Handle points to different DID. Expected {}, got {}", 711 expected, actual 712 ))) 713 .into_response(); 714 } 715 Err(e) => { 716 warn!("Handle verification failed: {}", e); 717 return ApiError::HandleNotAvailable(Some(format!( 718 "Handle verification failed: {}", 719 e 720 ))) 721 .into_response(); 722 } 723 } 724 new_handle.clone() 725 }; 726 let existing = sqlx::query!( 727 "SELECT id FROM users WHERE handle = $1 AND id != $2", 728 handle, 729 user_id 730 ) 731 .fetch_optional(&state.db) 732 .await; 733 if let Ok(Some(_)) = existing { 734 return ApiError::HandleTaken.into_response(); 735 } 736 let result = sqlx::query!( 737 "UPDATE users SET handle = $1 WHERE id = $2", 738 handle, 739 user_id 740 ) 741 .execute(&state.db) 742 .await; 743 match result { 744 Ok(_) => { 745 if !current_handle.is_empty() { 746 let _ = state 747 .cache 748 .delete(&format!("handle:{}", current_handle)) 749 .await; 750 } 751 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 752 if let Err(e) = 753 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await 754 { 755 warn!("Failed to sequence identity event for handle update: {}", e); 756 } 757 if let Err(e) = update_plc_handle(&state, &did, &handle).await { 758 warn!("Failed to update PLC handle: {}", e); 759 } 760 EmptyResponse::ok().into_response() 761 } 762 Err(e) => { 763 error!("DB error updating handle: {:?}", e); 764 ApiError::InternalError(None).into_response() 765 } 766 } 767} 768 769pub async fn update_plc_handle( 770 state: &AppState, 771 did: &str, 772 new_handle: &str, 773) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 774 if !did.starts_with("did:plc:") { 775 return Ok(()); 776 } 777 let user_row = sqlx::query!( 778 r#"SELECT u.id, uk.key_bytes, uk.encryption_version 779 FROM users u 780 JOIN user_keys uk ON u.id = uk.user_id 781 WHERE u.did = $1"#, 782 did 783 ) 784 .fetch_optional(&state.db) 785 .await?; 786 let user_row = match user_row { 787 Some(r) => r, 788 None => return Ok(()), 789 }; 790 let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?; 791 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?; 792 let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone())); 793 let last_op = plc_client.get_last_op(did).await?; 794 let new_also_known_as = vec![format!("at://{}", new_handle)]; 795 let update_op = 796 crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?; 797 let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?; 798 plc_client.send_operation(did, &signed_op).await?; 799 Ok(()) 800} 801 802pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 803 let host = match headers.get("host").and_then(|h| h.to_str().ok()) { 804 Some(h) => h, 805 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), 806 }; 807 let handle = host.split(':').next().unwrap_or(host); 808 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle) 809 .fetch_optional(&state.db) 810 .await; 811 match user { 812 Ok(Some(row)) => row.did.into_response(), 813 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(), 814 Err(e) => { 815 error!("DB error in well-known atproto-did: {:?}", e); 816 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() 817 } 818 } 819}