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