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