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