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