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)]
450pub struct Services {
451 pub atproto_pds: AtprotoPds,
452}
453
454#[derive(serde::Serialize)]
455#[serde(rename_all = "camelCase")]
456pub struct AtprotoPds {
457 #[serde(rename = "type")]
458 pub service_type: String,
459 pub endpoint: String,
460}
461
462pub async fn get_recommended_did_credentials(
463 State(state): State<AppState>,
464 headers: axum::http::HeaderMap,
465) -> Response {
466 let token = match crate::auth::extract_bearer_token_from_header(
467 headers.get("Authorization").and_then(|h| h.to_str().ok()),
468 ) {
469 Some(t) => t,
470 None => {
471 return (
472 StatusCode::UNAUTHORIZED,
473 Json(json!({"error": "AuthenticationRequired"})),
474 )
475 .into_response();
476 }
477 };
478 let auth_user =
479 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
480 Ok(user) => user,
481 Err(e) => return ApiError::from(e).into_response(),
482 };
483 let user = match sqlx::query!(
484 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1",
485 auth_user.did
486 )
487 .fetch_optional(&state.db)
488 .await
489 {
490 Ok(Some(row)) => row,
491 _ => return ApiError::InternalError.into_response(),
492 };
493 let key_bytes = match auth_user.key_bytes {
494 Some(kb) => kb,
495 None => {
496 return ApiError::AuthenticationFailedMsg(
497 "OAuth tokens cannot get DID credentials".into(),
498 )
499 .into_response();
500 }
501 };
502 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
503 let pds_endpoint = format!("https://{}", hostname);
504 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) {
505 Ok(k) => k,
506 Err(_) => return ApiError::InternalError.into_response(),
507 };
508 let did_key = signing_key_to_did_key(&signing_key);
509 let rotation_keys = if auth_user.did.starts_with("did:web:") {
510 vec![]
511 } else {
512 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
513 Ok(key) => key,
514 Err(_) => {
515 warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation");
516 did_key.clone()
517 }
518 };
519 vec![server_rotation_key]
520 };
521 (
522 StatusCode::OK,
523 Json(GetRecommendedDidCredentialsOutput {
524 rotation_keys,
525 also_known_as: vec![format!("at://{}", user.handle)],
526 verification_methods: VerificationMethods { atproto: did_key },
527 services: Services {
528 atproto_pds: AtprotoPds {
529 service_type: "AtprotoPersonalDataServer".to_string(),
530 endpoint: pds_endpoint,
531 },
532 },
533 }),
534 )
535 .into_response()
536}
537
538#[derive(Deserialize)]
539pub struct UpdateHandleInput {
540 pub handle: String,
541}
542
543pub async fn update_handle(
544 State(state): State<AppState>,
545 headers: axum::http::HeaderMap,
546 Json(input): Json<UpdateHandleInput>,
547) -> Response {
548 let token = match crate::auth::extract_bearer_token_from_header(
549 headers.get("Authorization").and_then(|h| h.to_str().ok()),
550 ) {
551 Some(t) => t,
552 None => return ApiError::AuthenticationRequired.into_response(),
553 };
554 let auth_user =
555 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
556 Ok(user) => user,
557 Err(e) => return ApiError::from(e).into_response(),
558 };
559 if let Err(e) = crate::auth::scope_check::check_identity_scope(
560 auth_user.is_oauth,
561 auth_user.scope.as_deref(),
562 crate::oauth::scopes::IdentityAttr::Handle,
563 ) {
564 return e;
565 }
566 let did = auth_user.did;
567 if !state
568 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did)
569 .await
570 {
571 return (
572 StatusCode::TOO_MANY_REQUESTS,
573 Json(json!({"error": "RateLimitExceeded", "message": "Too many handle updates. Try again later."})),
574 )
575 .into_response();
576 }
577 if !state
578 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did)
579 .await
580 {
581 return (
582 StatusCode::TOO_MANY_REQUESTS,
583 Json(json!({"error": "RateLimitExceeded", "message": "Daily handle update limit exceeded."})),
584 )
585 .into_response();
586 }
587 let user_row = match sqlx::query!(
588 "SELECT id, handle FROM users WHERE did = $1",
589 did
590 )
591 .fetch_optional(&state.db)
592 .await
593 {
594 Ok(Some(row)) => row,
595 _ => return ApiError::InternalError.into_response(),
596 };
597 let user_id = user_row.id;
598 let current_handle = user_row.handle;
599 let new_handle = input.handle.trim().to_ascii_lowercase();
600 if new_handle.is_empty() {
601 return ApiError::InvalidRequest("handle is required".into()).into_response();
602 }
603 if !new_handle
604 .chars()
605 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
606 {
607 return (
608 StatusCode::BAD_REQUEST,
609 Json(
610 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
611 ),
612 )
613 .into_response();
614 }
615 for segment in new_handle.split('.') {
616 if segment.is_empty() {
617 return (
618 StatusCode::BAD_REQUEST,
619 Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})),
620 )
621 .into_response();
622 }
623 if segment.starts_with('-') || segment.ends_with('-') {
624 return (
625 StatusCode::BAD_REQUEST,
626 Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})),
627 )
628 .into_response();
629 }
630 }
631 if crate::moderation::has_explicit_slur(&new_handle) {
632 return (
633 StatusCode::BAD_REQUEST,
634 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})),
635 )
636 .into_response();
637 }
638 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
639 let suffix = format!(".{}", hostname);
640 let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname);
641 let handle = if is_service_domain {
642 let short_part = if new_handle.ends_with(&suffix) {
643 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
644 } else {
645 &new_handle
646 };
647 let full_handle = if new_handle.ends_with(&suffix) {
648 new_handle.clone()
649 } else {
650 format!("{}.{}", new_handle, hostname)
651 };
652 if full_handle == current_handle {
653 if let Err(e) =
654 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
655 .await
656 {
657 warn!("Failed to sequence identity event for handle update: {}", e);
658 }
659 return (StatusCode::OK, Json(json!({}))).into_response();
660 }
661 if short_part.contains('.') {
662 return (
663 StatusCode::BAD_REQUEST,
664 Json(json!({
665 "error": "InvalidHandle",
666 "message": "Nested subdomains are not allowed. Use a simple handle without dots."
667 })),
668 )
669 .into_response();
670 }
671 if short_part.len() < 3 {
672 return (
673 StatusCode::BAD_REQUEST,
674 Json(json!({"error": "InvalidHandle", "message": "Handle too short"})),
675 )
676 .into_response();
677 }
678 if short_part.len() > 18 {
679 return (
680 StatusCode::BAD_REQUEST,
681 Json(json!({"error": "InvalidHandle", "message": "Handle too long"})),
682 )
683 .into_response();
684 }
685 full_handle
686 } else {
687 if new_handle == current_handle {
688 if let Err(e) =
689 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle))
690 .await
691 {
692 warn!("Failed to sequence identity event for handle update: {}", e);
693 }
694 return (StatusCode::OK, Json(json!({}))).into_response();
695 }
696 match crate::handle::verify_handle_ownership(&new_handle, &did).await {
697 Ok(()) => {}
698 Err(crate::handle::HandleResolutionError::NotFound) => {
699 return (
700 StatusCode::BAD_REQUEST,
701 Json(json!({
702 "error": "HandleNotAvailable",
703 "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did",
704 "handle": new_handle
705 })),
706 )
707 .into_response();
708 }
709 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
710 return (
711 StatusCode::BAD_REQUEST,
712 Json(json!({
713 "error": "HandleNotAvailable",
714 "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual)
715 })),
716 )
717 .into_response();
718 }
719 Err(e) => {
720 warn!("Handle verification failed: {}", e);
721 return (
722 StatusCode::BAD_REQUEST,
723 Json(json!({
724 "error": "HandleNotAvailable",
725 "message": format!("Handle verification failed: {}", e)
726 })),
727 )
728 .into_response();
729 }
730 }
731 new_handle.clone()
732 };
733 let existing = sqlx::query!(
734 "SELECT id FROM users WHERE handle = $1 AND id != $2",
735 handle,
736 user_id
737 )
738 .fetch_optional(&state.db)
739 .await;
740 if let Ok(Some(_)) = existing {
741 return (
742 StatusCode::BAD_REQUEST,
743 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
744 )
745 .into_response();
746 }
747 let result = sqlx::query!(
748 "UPDATE users SET handle = $1 WHERE id = $2",
749 handle,
750 user_id
751 )
752 .execute(&state.db)
753 .await;
754 match result {
755 Ok(_) => {
756 if !current_handle.is_empty() {
757 let _ = state.cache.delete(&format!("handle:{}", current_handle)).await;
758 }
759 let _ = state.cache.delete(&format!("handle:{}", handle)).await;
760 if let Err(e) =
761 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
762 {
763 warn!("Failed to sequence identity event for handle update: {}", e);
764 }
765 if let Err(e) = update_plc_handle(&state, &did, &handle).await {
766 warn!("Failed to update PLC handle: {}", e);
767 }
768 (StatusCode::OK, Json(json!({}))).into_response()
769 }
770 Err(e) => {
771 error!("DB error updating handle: {:?}", e);
772 (
773 StatusCode::INTERNAL_SERVER_ERROR,
774 Json(json!({"error": "InternalError"})),
775 )
776 .into_response()
777 }
778 }
779}
780
781pub async fn update_plc_handle(
782 state: &AppState,
783 did: &str,
784 new_handle: &str,
785) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
786 if !did.starts_with("did:plc:") {
787 return Ok(());
788 }
789 let user_row = sqlx::query!(
790 r#"SELECT u.id, uk.key_bytes, uk.encryption_version
791 FROM users u
792 JOIN user_keys uk ON u.id = uk.user_id
793 WHERE u.did = $1"#,
794 did
795 )
796 .fetch_optional(&state.db)
797 .await?;
798 let user_row = match user_row {
799 Some(r) => r,
800 None => return Ok(()),
801 };
802 let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
803 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
804 let plc_client = crate::plc::PlcClient::new(None);
805 let last_op = plc_client.get_last_op(did).await?;
806 let new_also_known_as = vec![format!("at://{}", new_handle)];
807 let update_op =
808 crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
809 let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
810 plc_client.send_operation(did, &signed_op).await?;
811 Ok(())
812}
813
814pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
815 let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
816 Some(h) => h,
817 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
818 };
819 let handle = host.split(':').next().unwrap_or(host);
820 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle)
821 .fetch_optional(&state.db)
822 .await;
823 match user {
824 Ok(Some(row)) => row.did.into_response(),
825 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
826 Err(e) => {
827 error!("DB error in well-known atproto-did: {:?}", e);
828 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
829 }
830 }
831}