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