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