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 vec![did_key.clone()]
515 };
516 (
517 StatusCode::OK,
518 Json(GetRecommendedDidCredentialsOutput {
519 rotation_keys,
520 also_known_as: vec![format!("at://{}", user.handle)],
521 verification_methods: VerificationMethods { atproto: did_key },
522 services: Services {
523 atproto_pds: AtprotoPds {
524 service_type: "AtprotoPersonalDataServer".to_string(),
525 endpoint: pds_endpoint,
526 },
527 },
528 }),
529 )
530 .into_response()
531}
532
533#[derive(Deserialize)]
534pub struct UpdateHandleInput {
535 pub handle: String,
536}
537
538pub async fn update_handle(
539 State(state): State<AppState>,
540 headers: axum::http::HeaderMap,
541 Json(input): Json<UpdateHandleInput>,
542) -> Response {
543 let token = match crate::auth::extract_bearer_token_from_header(
544 headers.get("Authorization").and_then(|h| h.to_str().ok()),
545 ) {
546 Some(t) => t,
547 None => return ApiError::AuthenticationRequired.into_response(),
548 };
549 let auth_user =
550 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
551 Ok(user) => user,
552 Err(e) => return ApiError::from(e).into_response(),
553 };
554 if let Err(e) = crate::auth::scope_check::check_identity_scope(
555 auth_user.is_oauth,
556 auth_user.scope.as_deref(),
557 crate::oauth::scopes::IdentityAttr::Handle,
558 ) {
559 return e;
560 }
561 let did = auth_user.did;
562 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
563 .fetch_optional(&state.db)
564 .await
565 {
566 Ok(Some(id)) => id,
567 _ => return ApiError::InternalError.into_response(),
568 };
569 let new_handle = input.handle.trim();
570 if new_handle.is_empty() {
571 return ApiError::InvalidRequest("handle is required".into()).into_response();
572 }
573 if !new_handle
574 .chars()
575 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
576 {
577 return (
578 StatusCode::BAD_REQUEST,
579 Json(
580 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
581 ),
582 )
583 .into_response();
584 }
585 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
586 let suffix = format!(".{}", hostname);
587 let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname);
588 let handle = if is_service_domain {
589 let short_part = if new_handle.ends_with(&suffix) {
590 new_handle.strip_suffix(&suffix).unwrap_or(new_handle)
591 } else {
592 new_handle
593 };
594 if short_part.contains('.') {
595 return (
596 StatusCode::BAD_REQUEST,
597 Json(json!({
598 "error": "InvalidHandle",
599 "message": "Nested subdomains are not allowed. Use a simple handle without dots."
600 })),
601 )
602 .into_response();
603 }
604 if new_handle.ends_with(&suffix) {
605 new_handle.to_string()
606 } else {
607 format!("{}.{}", new_handle, hostname)
608 }
609 } else {
610 match crate::handle::verify_handle_ownership(new_handle, &did).await {
611 Ok(()) => {}
612 Err(crate::handle::HandleResolutionError::NotFound) => {
613 return (
614 StatusCode::BAD_REQUEST,
615 Json(json!({
616 "error": "HandleNotAvailable",
617 "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did",
618 "handle": new_handle
619 })),
620 )
621 .into_response();
622 }
623 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
624 return (
625 StatusCode::BAD_REQUEST,
626 Json(json!({
627 "error": "HandleNotAvailable",
628 "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual)
629 })),
630 )
631 .into_response();
632 }
633 Err(e) => {
634 warn!("Handle verification failed: {}", e);
635 return (
636 StatusCode::BAD_REQUEST,
637 Json(json!({
638 "error": "HandleNotAvailable",
639 "message": format!("Handle verification failed: {}", e)
640 })),
641 )
642 .into_response();
643 }
644 }
645 new_handle.to_string()
646 };
647 let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id)
648 .fetch_optional(&state.db)
649 .await
650 .ok()
651 .flatten();
652 let existing = sqlx::query!(
653 "SELECT id FROM users WHERE handle = $1 AND id != $2",
654 handle,
655 user_id
656 )
657 .fetch_optional(&state.db)
658 .await;
659 if let Ok(Some(_)) = existing {
660 return (
661 StatusCode::BAD_REQUEST,
662 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
663 )
664 .into_response();
665 }
666 let result = sqlx::query!(
667 "UPDATE users SET handle = $1 WHERE id = $2",
668 handle,
669 user_id
670 )
671 .execute(&state.db)
672 .await;
673 match result {
674 Ok(_) => {
675 if let Some(old) = old_handle {
676 let _ = state.cache.delete(&format!("handle:{}", old)).await;
677 }
678 let _ = state.cache.delete(&format!("handle:{}", handle)).await;
679 if let Err(e) =
680 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
681 {
682 warn!("Failed to sequence identity event for handle update: {}", e);
683 }
684 if let Err(e) = update_plc_handle(&state, &did, &handle).await {
685 warn!("Failed to update PLC handle: {}", e);
686 }
687 (StatusCode::OK, Json(json!({}))).into_response()
688 }
689 Err(e) => {
690 error!("DB error updating handle: {:?}", e);
691 (
692 StatusCode::INTERNAL_SERVER_ERROR,
693 Json(json!({"error": "InternalError"})),
694 )
695 .into_response()
696 }
697 }
698}
699
700async fn update_plc_handle(
701 state: &AppState,
702 did: &str,
703 new_handle: &str,
704) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
705 if !did.starts_with("did:plc:") {
706 return Ok(());
707 }
708 let user_row = sqlx::query!(
709 r#"SELECT u.id, uk.key_bytes, uk.encryption_version
710 FROM users u
711 JOIN user_keys uk ON u.id = uk.user_id
712 WHERE u.did = $1"#,
713 did
714 )
715 .fetch_optional(&state.db)
716 .await?;
717 let user_row = match user_row {
718 Some(r) => r,
719 None => return Ok(()),
720 };
721 let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
722 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
723 let plc_client = crate::plc::PlcClient::new(None);
724 let last_op = plc_client.get_last_op(did).await?;
725 let new_also_known_as = vec![format!("at://{}", new_handle)];
726 let update_op =
727 crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
728 let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
729 plc_client.send_operation(did, &signed_op).await?;
730 Ok(())
731}
732
733pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
734 let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
735 Some(h) => h,
736 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
737 };
738 let handle = host.split(':').next().unwrap_or(host);
739 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle)
740 .fetch_optional(&state.db)
741 .await;
742 match user {
743 Ok(Some(row)) => row.did.into_response(),
744 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
745 Err(e) => {
746 error!("DB error in well-known atproto-did: {:?}", e);
747 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
748 }
749 }
750}