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(ref ovr) = overrides { 195 if let Ok(parsed) = 196 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 197 { 198 if !parsed.is_empty() { 199 let also_known_as = if !ovr.also_known_as.is_empty() { 200 ovr.also_known_as.clone() 201 } else { 202 vec![format!("at://{}", full_handle)] 203 }; 204 205 return Json(json!({ 206 "@context": [ 207 "https://www.w3.org/ns/did/v1", 208 "https://w3id.org/security/multikey/v1", 209 "https://w3id.org/security/suites/secp256k1-2019/v1" 210 ], 211 "id": did, 212 "alsoKnownAs": also_known_as, 213 "verificationMethod": parsed.iter().map(|m| json!({ 214 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 215 "type": m.method_type, 216 "controller": did, 217 "publicKeyMultibase": m.public_key_multibase 218 })).collect::<Vec<_>>(), 219 "service": [{ 220 "id": "#atproto_pds", 221 "type": "AtprotoPersonalDataServer", 222 "serviceEndpoint": service_endpoint 223 }] 224 })) 225 .into_response(); 226 } 227 } 228 } 229 230 let key_row = sqlx::query!( 231 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 232 user_id 233 ) 234 .fetch_optional(&state.db) 235 .await; 236 let key_bytes: Vec<u8> = match key_row { 237 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 238 Ok(k) => k, 239 Err(_) => { 240 return ( 241 StatusCode::INTERNAL_SERVER_ERROR, 242 Json(json!({"error": "InternalError"})), 243 ) 244 .into_response(); 245 } 246 }, 247 _ => { 248 return ( 249 StatusCode::INTERNAL_SERVER_ERROR, 250 Json(json!({"error": "InternalError"})), 251 ) 252 .into_response(); 253 } 254 }; 255 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 256 Ok(pk) => pk, 257 Err(e) => { 258 tracing::error!("Failed to generate public key multibase: {}", e); 259 return ( 260 StatusCode::INTERNAL_SERVER_ERROR, 261 Json(json!({"error": "InternalError"})), 262 ) 263 .into_response(); 264 } 265 }; 266 267 let also_known_as = if let Some(ref ovr) = overrides { 268 if !ovr.also_known_as.is_empty() { 269 ovr.also_known_as.clone() 270 } else { 271 vec![format!("at://{}", full_handle)] 272 } 273 } else { 274 vec![format!("at://{}", full_handle)] 275 }; 276 277 Json(json!({ 278 "@context": [ 279 "https://www.w3.org/ns/did/v1", 280 "https://w3id.org/security/multikey/v1", 281 "https://w3id.org/security/suites/secp256k1-2019/v1" 282 ], 283 "id": did, 284 "alsoKnownAs": also_known_as, 285 "verificationMethod": [{ 286 "id": format!("{}#atproto", did), 287 "type": "Multikey", 288 "controller": did, 289 "publicKeyMultibase": public_key_multibase 290 }], 291 "service": [{ 292 "id": "#atproto_pds", 293 "type": "AtprotoPersonalDataServer", 294 "serviceEndpoint": service_endpoint 295 }] 296 })) 297 .into_response() 298} 299 300pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 301 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 302 let full_handle = format!("{}.{}", handle, hostname); 303 let user = sqlx::query!( 304 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 305 full_handle 306 ) 307 .fetch_optional(&state.db) 308 .await; 309 let (user_id, did, migrated_to_pds) = match user { 310 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 311 Ok(None) => { 312 return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 313 } 314 Err(e) => { 315 error!("DB Error: {:?}", e); 316 return ( 317 StatusCode::INTERNAL_SERVER_ERROR, 318 Json(json!({"error": "InternalError"})), 319 ) 320 .into_response(); 321 } 322 }; 323 if !did.starts_with("did:web:") { 324 return ( 325 StatusCode::NOT_FOUND, 326 Json(json!({"error": "NotFound", "message": "User is not did:web"})), 327 ) 328 .into_response(); 329 } 330 let encoded_hostname = hostname.replace(':', "%3A"); 331 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle); 332 let subdomain_host = format!("{}.{}", handle, hostname); 333 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 334 let new_subdomain_format = format!("did:web:{}", encoded_subdomain); 335 if did != old_path_format && did != new_subdomain_format { 336 return ( 337 StatusCode::NOT_FOUND, 338 Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})), 339 ) 340 .into_response(); 341 } 342 343 let overrides = sqlx::query!( 344 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 345 user_id 346 ) 347 .fetch_optional(&state.db) 348 .await 349 .ok() 350 .flatten(); 351 352 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 353 354 if let Some(ref ovr) = overrides { 355 if let Ok(parsed) = 356 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 357 { 358 if !parsed.is_empty() { 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 } 389 390 let key_row = sqlx::query!( 391 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 392 user_id 393 ) 394 .fetch_optional(&state.db) 395 .await; 396 let key_bytes: Vec<u8> = match key_row { 397 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 398 Ok(k) => k, 399 Err(_) => { 400 return ( 401 StatusCode::INTERNAL_SERVER_ERROR, 402 Json(json!({"error": "InternalError"})), 403 ) 404 .into_response(); 405 } 406 }, 407 _ => { 408 return ( 409 StatusCode::INTERNAL_SERVER_ERROR, 410 Json(json!({"error": "InternalError"})), 411 ) 412 .into_response(); 413 } 414 }; 415 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 416 Ok(pk) => pk, 417 Err(e) => { 418 tracing::error!("Failed to generate public key multibase: {}", e); 419 return ( 420 StatusCode::INTERNAL_SERVER_ERROR, 421 Json(json!({"error": "InternalError"})), 422 ) 423 .into_response(); 424 } 425 }; 426 427 let also_known_as = if let Some(ref ovr) = overrides { 428 if !ovr.also_known_as.is_empty() { 429 ovr.also_known_as.clone() 430 } else { 431 vec![format!("at://{}", full_handle)] 432 } 433 } else { 434 vec![format!("at://{}", full_handle)] 435 }; 436 437 Json(json!({ 438 "@context": [ 439 "https://www.w3.org/ns/did/v1", 440 "https://w3id.org/security/multikey/v1", 441 "https://w3id.org/security/suites/secp256k1-2019/v1" 442 ], 443 "id": did, 444 "alsoKnownAs": also_known_as, 445 "verificationMethod": [{ 446 "id": format!("{}#atproto", did), 447 "type": "Multikey", 448 "controller": did, 449 "publicKeyMultibase": public_key_multibase 450 }], 451 "service": [{ 452 "id": "#atproto_pds", 453 "type": "AtprotoPersonalDataServer", 454 "serviceEndpoint": service_endpoint 455 }] 456 })) 457 .into_response() 458} 459 460pub async fn verify_did_web( 461 did: &str, 462 hostname: &str, 463 handle: &str, 464 expected_signing_key: Option<&str>, 465) -> Result<(), String> { 466 let subdomain_host = format!("{}.{}", handle, hostname); 467 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 468 let expected_subdomain_did = format!("did:web:{}", encoded_subdomain); 469 if did == expected_subdomain_did { 470 return Ok(()); 471 } 472 let expected_prefix = if hostname.contains(':') { 473 format!("did:web:{}", hostname.replace(':', "%3A")) 474 } else { 475 format!("did:web:{}", hostname) 476 }; 477 if did.starts_with(&expected_prefix) { 478 let suffix = &did[expected_prefix.len()..]; 479 let expected_suffix = format!(":u:{}", handle); 480 if suffix == expected_suffix { 481 return Ok(()); 482 } else { 483 return Err(format!( 484 "Invalid DID path for this PDS. Expected {}", 485 expected_suffix 486 )); 487 } 488 } 489 let expected_signing_key = expected_signing_key.ok_or_else(|| { 490 "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() 491 })?; 492 let parts: Vec<&str> = did.split(':').collect(); 493 if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 494 return Err("Invalid did:web format".into()); 495 } 496 let domain_segment = parts[2]; 497 let domain = domain_segment.replace("%3A", ":"); 498 let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 499 "http" 500 } else { 501 "https" 502 }; 503 let url = if parts.len() == 3 { 504 format!("{}://{}/.well-known/did.json", scheme, domain) 505 } else { 506 let path = parts[3..].join("/"); 507 format!("{}://{}/{}/did.json", scheme, domain, path) 508 }; 509 let client = reqwest::Client::builder() 510 .timeout(std::time::Duration::from_secs(5)) 511 .build() 512 .map_err(|e| format!("Failed to create client: {}", e))?; 513 let resp = client 514 .get(&url) 515 .send() 516 .await 517 .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 518 if !resp.status().is_success() { 519 return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 520 } 521 let doc: serde_json::Value = resp 522 .json() 523 .await 524 .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 525 let services = doc["service"] 526 .as_array() 527 .ok_or("No services found in DID doc")?; 528 let pds_endpoint = format!("https://{}", hostname); 529 let has_valid_service = services 530 .iter() 531 .any(|s| s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint); 532 if !has_valid_service { 533 return Err(format!( 534 "DID document does not list this PDS ({}) as AtprotoPersonalDataServer", 535 pds_endpoint 536 )); 537 } 538 let verification_methods = doc["verificationMethod"] 539 .as_array() 540 .ok_or("No verificationMethod found in DID doc")?; 541 let expected_multibase = expected_signing_key 542 .strip_prefix("did:key:") 543 .ok_or("Invalid signing key format")?; 544 let has_matching_key = verification_methods.iter().any(|vm| { 545 vm["publicKeyMultibase"] 546 .as_str() 547 .map(|pk| pk == expected_multibase) 548 .unwrap_or(false) 549 }); 550 if !has_matching_key { 551 return Err(format!( 552 "DID document verification key does not match reserved signing key. Expected publicKeyMultibase: {}", 553 expected_multibase 554 )); 555 } 556 Ok(()) 557} 558 559#[derive(serde::Serialize)] 560#[serde(rename_all = "camelCase")] 561pub struct GetRecommendedDidCredentialsOutput { 562 pub rotation_keys: Vec<String>, 563 pub also_known_as: Vec<String>, 564 pub verification_methods: VerificationMethods, 565 pub services: Services, 566} 567 568#[derive(serde::Serialize)] 569#[serde(rename_all = "camelCase")] 570pub struct VerificationMethods { 571 pub atproto: String, 572} 573 574#[derive(serde::Serialize)] 575pub struct Services { 576 pub atproto_pds: AtprotoPds, 577} 578 579#[derive(serde::Serialize)] 580#[serde(rename_all = "camelCase")] 581pub struct AtprotoPds { 582 #[serde(rename = "type")] 583 pub service_type: String, 584 pub endpoint: String, 585} 586 587pub async fn get_recommended_did_credentials( 588 State(state): State<AppState>, 589 headers: axum::http::HeaderMap, 590) -> Response { 591 let token = match crate::auth::extract_bearer_token_from_header( 592 headers.get("Authorization").and_then(|h| h.to_str().ok()), 593 ) { 594 Some(t) => t, 595 None => { 596 return ( 597 StatusCode::UNAUTHORIZED, 598 Json(json!({"error": "AuthenticationRequired"})), 599 ) 600 .into_response(); 601 } 602 }; 603 let auth_user = 604 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 605 Ok(user) => user, 606 Err(e) => return ApiError::from(e).into_response(), 607 }; 608 let user = match sqlx::query!( 609 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 610 auth_user.did 611 ) 612 .fetch_optional(&state.db) 613 .await 614 { 615 Ok(Some(row)) => row, 616 _ => return ApiError::InternalError.into_response(), 617 }; 618 let key_bytes = match auth_user.key_bytes { 619 Some(kb) => kb, 620 None => { 621 return ApiError::AuthenticationFailedMsg( 622 "OAuth tokens cannot get DID credentials".into(), 623 ) 624 .into_response(); 625 } 626 }; 627 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 628 let pds_endpoint = format!("https://{}", hostname); 629 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 630 Ok(k) => k, 631 Err(_) => return ApiError::InternalError.into_response(), 632 }; 633 let did_key = signing_key_to_did_key(&signing_key); 634 let rotation_keys = if auth_user.did.starts_with("did:web:") { 635 vec![] 636 } else { 637 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { 638 Ok(key) => key, 639 Err(_) => { 640 warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"); 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!( 713 "SELECT id, handle FROM users WHERE did = $1", 714 did 715 ) 716 .fetch_optional(&state.db) 717 .await 718 { 719 Ok(Some(row)) => row, 720 _ => return ApiError::InternalError.into_response(), 721 }; 722 let user_id = user_row.id; 723 let current_handle = user_row.handle; 724 let new_handle = input.handle.trim().to_ascii_lowercase(); 725 if new_handle.is_empty() { 726 return ApiError::InvalidRequest("handle is required".into()).into_response(); 727 } 728 if !new_handle 729 .chars() 730 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 731 { 732 return ( 733 StatusCode::BAD_REQUEST, 734 Json( 735 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 736 ), 737 ) 738 .into_response(); 739 } 740 for segment in new_handle.split('.') { 741 if segment.is_empty() { 742 return ( 743 StatusCode::BAD_REQUEST, 744 Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})), 745 ) 746 .into_response(); 747 } 748 if segment.starts_with('-') || segment.ends_with('-') { 749 return ( 750 StatusCode::BAD_REQUEST, 751 Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})), 752 ) 753 .into_response(); 754 } 755 } 756 if crate::moderation::has_explicit_slur(&new_handle) { 757 return ( 758 StatusCode::BAD_REQUEST, 759 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), 760 ) 761 .into_response(); 762 } 763 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 764 let suffix = format!(".{}", hostname); 765 let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname); 766 let handle = if is_service_domain { 767 let short_part = if new_handle.ends_with(&suffix) { 768 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 769 } else { 770 &new_handle 771 }; 772 let full_handle = if new_handle.ends_with(&suffix) { 773 new_handle.clone() 774 } else { 775 format!("{}.{}", new_handle, hostname) 776 }; 777 if full_handle == current_handle { 778 if let Err(e) = 779 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)) 780 .await 781 { 782 warn!("Failed to sequence identity event for handle update: {}", e); 783 } 784 return (StatusCode::OK, Json(json!({}))).into_response(); 785 } 786 if short_part.contains('.') { 787 return ( 788 StatusCode::BAD_REQUEST, 789 Json(json!({ 790 "error": "InvalidHandle", 791 "message": "Nested subdomains are not allowed. Use a simple handle without dots." 792 })), 793 ) 794 .into_response(); 795 } 796 if short_part.len() < 3 { 797 return ( 798 StatusCode::BAD_REQUEST, 799 Json(json!({"error": "InvalidHandle", "message": "Handle too short"})), 800 ) 801 .into_response(); 802 } 803 if short_part.len() > 18 { 804 return ( 805 StatusCode::BAD_REQUEST, 806 Json(json!({"error": "InvalidHandle", "message": "Handle too long"})), 807 ) 808 .into_response(); 809 } 810 full_handle 811 } else { 812 if new_handle == current_handle { 813 if let Err(e) = 814 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle)) 815 .await 816 { 817 warn!("Failed to sequence identity event for handle update: {}", e); 818 } 819 return (StatusCode::OK, Json(json!({}))).into_response(); 820 } 821 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 822 Ok(()) => {} 823 Err(crate::handle::HandleResolutionError::NotFound) => { 824 return ( 825 StatusCode::BAD_REQUEST, 826 Json(json!({ 827 "error": "HandleNotAvailable", 828 "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did", 829 "handle": new_handle 830 })), 831 ) 832 .into_response(); 833 } 834 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 835 return ( 836 StatusCode::BAD_REQUEST, 837 Json(json!({ 838 "error": "HandleNotAvailable", 839 "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual) 840 })), 841 ) 842 .into_response(); 843 } 844 Err(e) => { 845 warn!("Handle verification failed: {}", e); 846 return ( 847 StatusCode::BAD_REQUEST, 848 Json(json!({ 849 "error": "HandleNotAvailable", 850 "message": format!("Handle verification failed: {}", e) 851 })), 852 ) 853 .into_response(); 854 } 855 } 856 new_handle.clone() 857 }; 858 let existing = sqlx::query!( 859 "SELECT id FROM users WHERE handle = $1 AND id != $2", 860 handle, 861 user_id 862 ) 863 .fetch_optional(&state.db) 864 .await; 865 if let Ok(Some(_)) = existing { 866 return ( 867 StatusCode::BAD_REQUEST, 868 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 869 ) 870 .into_response(); 871 } 872 let result = sqlx::query!( 873 "UPDATE users SET handle = $1 WHERE id = $2", 874 handle, 875 user_id 876 ) 877 .execute(&state.db) 878 .await; 879 match result { 880 Ok(_) => { 881 if !current_handle.is_empty() { 882 let _ = state.cache.delete(&format!("handle:{}", current_handle)).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}