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