forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
27 kB
742 lines
1use crate::common;
2use axum::{
3 Json,
4 extract::{Path, Query, State},
5 http::{HeaderMap, StatusCode},
6 response::{IntoResponse, Response},
7};
8use base64::Engine;
9use k256::SecretKey;
10use k256::elliptic_curve::sec1::ToEncodedPoint;
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use tracing::{error, warn};
14use tranquil_pds::api::error::DbResultExt;
15use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse};
16use tranquil_pds::auth::{Auth, NotTakendown};
17use tranquil_pds::plc::signing_key_to_did_key;
18use tranquil_pds::rate_limit::{
19 HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message,
20};
21use tranquil_pds::state::AppState;
22use tranquil_pds::types::{Did, Handle};
23use tranquil_pds::util::get_header_str;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct DidWebVerificationMethod {
28 pub id: String,
29 #[serde(rename = "type")]
30 pub method_type: String,
31 pub public_key_multibase: String,
32}
33
34#[derive(Deserialize)]
35pub struct ResolveHandleParams {
36 pub handle: String,
37}
38
39pub async fn resolve_handle(
40 State(state): State<AppState>,
41 Query(params): Query<ResolveHandleParams>,
42) -> Response {
43 let handle_str = params.handle.trim();
44 if handle_str.is_empty() {
45 return ApiError::InvalidRequest("handle is required".into()).into_response();
46 }
47 let handle: Handle = match handle_str.parse() {
48 Ok(h) => h,
49 Err(_) => {
50 return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
51 }
52 };
53 let cache_key = tranquil_pds::cache_keys::handle_key(&handle);
54 if let Some(did) = state.cache.get(&cache_key).await {
55 return DidResponse::response(did).into_response();
56 }
57 let user = state.repos.user.get_by_handle(&handle).await;
58 match user {
59 Ok(Some(row)) => {
60 let _ = state
61 .cache
62 .set(&cache_key, &row.did, std::time::Duration::from_secs(300))
63 .await;
64 DidResponse::response(row.did).into_response()
65 }
66 Ok(None) => match tranquil_pds::handle::resolve_handle(&handle).await {
67 Ok(did) => {
68 let _ = state
69 .cache
70 .set(&cache_key, &did, std::time::Duration::from_secs(300))
71 .await;
72 DidResponse::response(did).into_response()
73 }
74 Err(_) => ApiError::HandleNotFound.into_response(),
75 },
76 Err(e) => {
77 error!("DB error resolving handle: {:?}", e);
78 ApiError::InternalError(None).into_response()
79 }
80 }
81}
82
83#[derive(Debug)]
84pub enum KeyError {
85 InvalidKeyLength,
86 MissingCoordinate,
87}
88
89impl std::fmt::Display for KeyError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::InvalidKeyLength => write!(f, "invalid key length"),
93 Self::MissingCoordinate => write!(f, "missing elliptic curve coordinate"),
94 }
95 }
96}
97
98impl std::error::Error for KeyError {}
99
100pub fn get_jwk(key_bytes: &[u8]) -> Result<serde_json::Value, KeyError> {
101 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| KeyError::InvalidKeyLength)?;
102 let public_key = secret_key.public_key();
103 let encoded = public_key.to_encoded_point(false);
104 let x = encoded.x().ok_or(KeyError::MissingCoordinate)?;
105 let y = encoded.y().ok_or(KeyError::MissingCoordinate)?;
106 let x_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x);
107 let y_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y);
108 Ok(json!({
109 "kty": "EC",
110 "crv": "secp256k1",
111 "x": x_b64,
112 "y": y_b64
113 }))
114}
115
116pub fn get_public_key_multibase(key_bytes: &[u8]) -> Result<String, KeyError> {
117 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| KeyError::InvalidKeyLength)?;
118 let public_key = secret_key.public_key();
119 let compressed = public_key.to_encoded_point(true);
120 let compressed_bytes = compressed.as_bytes();
121 let mut multicodec_key = vec![0xe7, 0x01];
122 multicodec_key.extend_from_slice(compressed_bytes);
123 Ok(format!("z{}", bs58::encode(&multicodec_key).into_string()))
124}
125
126pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
127 let cfg = tranquil_config::get();
128 let hostname = &cfg.server.hostname;
129 let hostname_without_port = cfg.server.hostname_without_port();
130 let host_header = get_header_str(&headers, http::header::HOST).unwrap_or(hostname);
131 let host_without_port = host_header.split(':').next().unwrap_or(host_header);
132 if host_without_port != hostname_without_port {
133 let is_subdomain = cfg
134 .server
135 .available_user_domain_list()
136 .into_iter()
137 .chain(std::iter::once(hostname_without_port.to_string()))
138 .any(|d| host_without_port.ends_with(&format!(".{}", d)));
139 if is_subdomain {
140 return serve_handle_did_doc(&state, host_without_port, hostname).await;
141 }
142 }
143 let did = if hostname.contains(':') {
144 format!("did:web:{}", hostname.replace(':', "%3A"))
145 } else {
146 format!("did:web:{}", hostname)
147 };
148 Json(json!({
149 "@context": ["https://www.w3.org/ns/did/v1"],
150 "id": did,
151 "service": [{
152 "id": "#atproto_pds",
153 "type": tranquil_pds::plc::ServiceType::Pds.as_str(),
154 "serviceEndpoint": format!("https://{}", hostname)
155 }]
156 }))
157 .into_response()
158}
159
160async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
161 let encoded_handle = handle.replace(':', "%3A");
162 let expected_did: tranquil_pds::types::Did = match format!("did:web:{}", encoded_handle).parse()
163 {
164 Ok(d) => d,
165 Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
166 };
167 let user = match state
168 .repos
169 .user
170 .get_user_for_did_doc_build(&expected_did)
171 .await
172 {
173 Ok(Some(u)) => u,
174 Ok(None) => {
175 return ApiError::NotFoundMsg("User not found".into()).into_response();
176 }
177 Err(e) => {
178 error!("DB Error: {:?}", e);
179 return ApiError::InternalError(None).into_response();
180 }
181 };
182 let (user_id, current_handle, migrated_to_pds) = (user.id, user.handle, user.migrated_to_pds);
183 let did = expected_did;
184
185 let overrides = state
186 .repos
187 .user
188 .get_did_web_overrides(user_id)
189 .await
190 .ok()
191 .flatten();
192
193 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
194
195 let verification_methods =
196 build_override_or_key_verification_methods(state, user_id, &did, overrides.as_ref()).await;
197 let verification_methods = match verification_methods {
198 Ok(vm) => vm,
199 Err(resp) => return resp,
200 };
201
202 let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle);
203 Json(common::build_did_document(
204 &did,
205 also_known_as,
206 verification_methods,
207 &service_endpoint,
208 ))
209 .into_response()
210}
211
212pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
213 let hostname = &tranquil_config::get().server.hostname;
214 let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
215 let current_handle: Handle = match format!("{}.{}", handle, hostname_for_handles).parse() {
216 Ok(h) => h,
217 Err(_) => {
218 return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
219 }
220 };
221 let user = match state
222 .repos
223 .user
224 .get_did_web_info_by_handle(¤t_handle)
225 .await
226 {
227 Ok(Some(u)) => u,
228 Ok(None) => {
229 return ApiError::NotFoundMsg("User not found".into()).into_response();
230 }
231 Err(e) => {
232 error!("DB Error: {:?}", e);
233 return ApiError::InternalError(None).into_response();
234 }
235 };
236 let (user_id, did, migrated_to_pds) = (user.id, user.did, user.migrated_to_pds);
237 if !did.starts_with("did:web:") {
238 return ApiError::NotFoundMsg("User is not did:web".into()).into_response();
239 }
240 let encoded_hostname = hostname.replace(':', "%3A");
241 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle);
242 let subdomain_host = format!("{}.{}", handle, hostname_for_handles);
243 let encoded_subdomain = subdomain_host.replace(':', "%3A");
244 let new_subdomain_format = format!("did:web:{}", encoded_subdomain);
245 if did != old_path_format && did != new_subdomain_format {
246 return ApiError::NotFoundMsg("External did:web - DID document hosted by user".into())
247 .into_response();
248 }
249
250 let overrides = state
251 .repos
252 .user
253 .get_did_web_overrides(user_id)
254 .await
255 .ok()
256 .flatten();
257
258 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
259
260 let verification_methods =
261 build_override_or_key_verification_methods(&state, user_id, &did, overrides.as_ref()).await;
262 let verification_methods = match verification_methods {
263 Ok(vm) => vm,
264 Err(resp) => return resp,
265 };
266
267 let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle);
268 Json(common::build_did_document(
269 &did,
270 also_known_as,
271 verification_methods,
272 &service_endpoint,
273 ))
274 .into_response()
275}
276
277async fn build_override_or_key_verification_methods(
278 state: &AppState,
279 user_id: uuid::Uuid,
280 did: &str,
281 overrides: Option<&tranquil_db_traits::DidWebOverrides>,
282) -> Result<Vec<serde_json::Value>, Response> {
283 if let Some(parsed) = overrides.and_then(|ovr| {
284 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone())
285 .ok()
286 .filter(|p| !p.is_empty())
287 }) {
288 return Ok(parsed
289 .iter()
290 .map(|m| {
291 json!({
292 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }),
293 "type": m.method_type,
294 "controller": did,
295 "publicKeyMultibase": m.public_key_multibase
296 })
297 })
298 .collect());
299 }
300
301 let key_info = match state.repos.user.get_user_key_by_id(user_id).await {
302 Ok(Some(k)) => k,
303 _ => return Err(ApiError::InternalError(None).into_response()),
304 };
305 let key_bytes =
306 tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
307 .map_err(|_| ApiError::InternalError(None).into_response())?;
308 let public_key_multibase = get_public_key_multibase(&key_bytes).map_err(|e| {
309 tracing::error!("Failed to generate public key multibase: {}", e);
310 ApiError::InternalError(None).into_response()
311 })?;
312
313 Ok(vec![json!({
314 "id": format!("{}#atproto", did),
315 "type": "Multikey",
316 "controller": did,
317 "publicKeyMultibase": public_key_multibase
318 })])
319}
320
321#[derive(Debug, thiserror::Error)]
322pub enum DidWebVerifyError {
323 #[error("Invalid did:web format")]
324 InvalidFormat,
325 #[error("Invalid DID path for this PDS. Expected {0}")]
326 InvalidPath(String),
327 #[error(
328 "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."
329 )]
330 MissingSigningKey,
331 #[error("Failed to fetch DID doc: {0}")]
332 FetchFailed(String),
333 #[error("Invalid DID document: {0}")]
334 InvalidDocument(String),
335 #[error("DID document does not list this PDS ({0}) as AtprotoPersonalDataServer")]
336 PdsNotListed(String),
337 #[error(
338 "DID document verification key does not match reserved signing key. Expected publicKeyMultibase: {0}"
339 )]
340 KeyMismatch(String),
341 #[error("Invalid signing key format")]
342 InvalidSigningKey,
343}
344
345pub async fn verify_did_web(
346 did: &str,
347 hostname: &str,
348 handle: &str,
349 expected_signing_key: Option<&Did>,
350) -> Result<(), DidWebVerifyError> {
351 let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname);
352 let subdomain_host = format!("{}.{}", handle, hostname_for_handles);
353 let encoded_subdomain = subdomain_host.replace(':', "%3A");
354 let expected_subdomain_did = format!("did:web:{}", encoded_subdomain);
355 if did == expected_subdomain_did {
356 return Ok(());
357 }
358 let expected_prefix = if hostname.contains(':') {
359 format!("did:web:{}", hostname.replace(':', "%3A"))
360 } else {
361 format!("did:web:{}", hostname)
362 };
363 if did.starts_with(&expected_prefix) {
364 let suffix = &did[expected_prefix.len()..];
365 let expected_suffix = format!(":u:{}", handle);
366 return if suffix == expected_suffix {
367 Ok(())
368 } else {
369 Err(DidWebVerifyError::InvalidPath(expected_suffix))
370 };
371 }
372 let expected_signing_key = expected_signing_key.ok_or(DidWebVerifyError::MissingSigningKey)?;
373 let parts: Vec<&str> = did.split(':').collect();
374 if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" {
375 return Err(DidWebVerifyError::InvalidFormat);
376 }
377 let domain_segment = parts[2];
378 let domain = domain_segment.replace("%3A", ":");
379 let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") {
380 "http"
381 } else {
382 "https"
383 };
384 let url = if parts.len() == 3 {
385 format!("{}://{}/.well-known/did.json", scheme, domain)
386 } else {
387 let path = parts[3..].join("/");
388 format!("{}://{}/{}/did.json", scheme, domain, path)
389 };
390 let client = tranquil_pds::api::proxy_client::did_resolution_client();
391 let resp = client
392 .get(&url)
393 .send()
394 .await
395 .map_err(|e| DidWebVerifyError::FetchFailed(e.to_string()))?;
396 if !resp.status().is_success() {
397 return Err(DidWebVerifyError::FetchFailed(format!(
398 "HTTP {}",
399 resp.status()
400 )));
401 }
402 let doc: serde_json::Value = resp
403 .json()
404 .await
405 .map_err(|e| DidWebVerifyError::InvalidDocument(e.to_string()))?;
406 let services = doc["service"]
407 .as_array()
408 .ok_or(DidWebVerifyError::InvalidDocument(
409 "No services found".to_string(),
410 ))?;
411 let pds_endpoint = format!("https://{}", hostname);
412 let has_valid_service = services.iter().any(|s| {
413 s["type"] == tranquil_pds::plc::ServiceType::Pds.as_str()
414 && s["serviceEndpoint"] == pds_endpoint
415 });
416 if !has_valid_service {
417 return Err(DidWebVerifyError::PdsNotListed(pds_endpoint));
418 }
419 let verification_methods =
420 doc["verificationMethod"]
421 .as_array()
422 .ok_or(DidWebVerifyError::InvalidDocument(
423 "No verificationMethod found".to_string(),
424 ))?;
425 let expected_multibase = expected_signing_key
426 .strip_prefix("did:key:")
427 .ok_or(DidWebVerifyError::InvalidSigningKey)?;
428 let has_matching_key = verification_methods.iter().any(|vm| {
429 vm["publicKeyMultibase"]
430 .as_str()
431 .is_some_and(|pk| pk == expected_multibase)
432 });
433 if !has_matching_key {
434 return Err(DidWebVerifyError::KeyMismatch(
435 expected_multibase.to_string(),
436 ));
437 }
438 Ok(())
439}
440
441#[derive(serde::Serialize)]
442#[serde(rename_all = "camelCase")]
443pub struct GetRecommendedDidCredentialsOutput {
444 pub rotation_keys: Vec<String>,
445 pub also_known_as: Vec<String>,
446 pub verification_methods: VerificationMethods,
447 pub services: Services,
448}
449
450#[derive(serde::Serialize)]
451#[serde(rename_all = "camelCase")]
452pub struct VerificationMethods {
453 pub atproto: String,
454}
455
456#[derive(serde::Serialize)]
457pub struct Services {
458 pub atproto_pds: AtprotoPds,
459}
460
461#[derive(serde::Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct AtprotoPds {
464 #[serde(rename = "type")]
465 pub service_type: String,
466 pub endpoint: String,
467}
468
469pub async fn get_recommended_did_credentials(
470 State(state): State<AppState>,
471 auth: Auth<NotTakendown>,
472) -> Result<Json<GetRecommendedDidCredentialsOutput>, ApiError> {
473 let handle = state
474 .repos
475 .user
476 .get_handle_by_did(&auth.did)
477 .await
478 .log_db_err("fetching handle for DID credentials")?
479 .ok_or(ApiError::InternalError(None))?;
480
481 let key_bytes = auth.key_bytes.clone().ok_or_else(|| {
482 ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into()))
483 })?;
484
485 let hostname = &tranquil_config::get().server.hostname;
486 let pds_endpoint = format!("https://{}", hostname);
487 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)
488 .map_err(|_| ApiError::InternalError(None))?;
489 let did_key = signing_key_to_did_key(&signing_key);
490 let rotation_keys = if auth.did.starts_with("did:web:") {
491 vec![]
492 } else {
493 tranquil_pds::plc::rotation_keys_for(
494 tranquil_config::get().secrets.plc_rotation_key.as_deref(),
495 &signing_key,
496 )
497 };
498 Ok(Json(GetRecommendedDidCredentialsOutput {
499 rotation_keys,
500 also_known_as: vec![format!("at://{}", handle)],
501 verification_methods: VerificationMethods { atproto: did_key },
502 services: Services {
503 atproto_pds: AtprotoPds {
504 service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
505 endpoint: pds_endpoint,
506 },
507 },
508 }))
509}
510
511#[derive(Deserialize)]
512pub struct UpdateHandleInput {
513 pub handle: String,
514}
515
516pub async fn update_handle(
517 State(state): State<AppState>,
518 auth: Auth<NotTakendown>,
519 Json(input): Json<UpdateHandleInput>,
520) -> Result<Json<EmptyResponse>, ApiError> {
521 tranquil_pds::auth::scope_check::check_identity_scope(
522 &auth.auth_source,
523 auth.scope.as_deref(),
524 tranquil_pds::oauth::scopes::IdentityAttr::Handle,
525 )?;
526 let did = auth.did.clone();
527 let _rate_limit = check_user_rate_limit_with_message::<HandleUpdateLimit>(
528 &state,
529 &did,
530 "Too many handle updates. Try again later.",
531 )
532 .await?;
533 let _daily_rate_limit = check_user_rate_limit_with_message::<HandleUpdateDailyLimit>(
534 &state,
535 &did,
536 "Daily handle update limit exceeded.",
537 )
538 .await?;
539 let user_row = state
540 .repos
541 .user
542 .get_id_and_handle_by_did(&did)
543 .await
544 .log_db_err("fetching user for handle update")?
545 .ok_or(ApiError::InternalError(None))?;
546 let user_id = user_row.id;
547 let current_handle = user_row.handle;
548 let new_handle = input.handle.trim().to_ascii_lowercase();
549 if new_handle.is_empty() {
550 return Err(ApiError::InvalidRequest("handle is required".into()));
551 }
552 if !new_handle
553 .chars()
554 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
555 {
556 return Err(ApiError::InvalidHandle(Some(
557 "Handle contains invalid characters".into(),
558 )));
559 }
560 if new_handle.split('.').any(|segment| segment.is_empty()) {
561 return Err(ApiError::InvalidHandle(Some(
562 "Handle contains empty segment".into(),
563 )));
564 }
565 if new_handle
566 .split('.')
567 .any(|segment| segment.starts_with('-') || segment.ends_with('-'))
568 {
569 return Err(ApiError::InvalidHandle(Some(
570 "Handle segment cannot start or end with hyphen".into(),
571 )));
572 }
573 if tranquil_pds::moderation::has_explicit_slur(&new_handle) {
574 return Err(ApiError::InvalidHandle(Some(
575 "Inappropriate language in handle".into(),
576 )));
577 }
578 let handle_domains = tranquil_config::get().server.user_handle_domain_list();
579 let matched_handle_domain = handle_domains
580 .iter()
581 .filter(|d| new_handle.ends_with(&format!(".{}", d)))
582 .max_by_key(|d| d.len())
583 .cloned();
584 let is_domain_itself = handle_domains.iter().any(|d| d == &new_handle);
585 let handle: Handle = if (!new_handle.contains('.') || matched_handle_domain.is_some())
586 && !is_domain_itself
587 {
588 let (short_part, full_handle) = match &matched_handle_domain {
589 Some(domain) => {
590 let suffix = format!(".{}", domain);
591 let short = new_handle.strip_suffix(&suffix).unwrap_or(&new_handle);
592 (short.to_string(), new_handle.clone())
593 }
594 None => {
595 let primary = &handle_domains[0];
596 (new_handle.clone(), format!("{}.{}", new_handle, primary))
597 }
598 };
599 if full_handle == current_handle {
600 let handle: Handle = match full_handle.parse() {
601 Ok(h) => h,
602 Err(_) => return Err(ApiError::InvalidHandle(None)),
603 };
604 if let Err(e) =
605 tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
606 {
607 warn!("Failed to sequence identity event for handle update: {}", e);
608 }
609 return Ok(Json(EmptyResponse {}));
610 }
611 if short_part.contains('.') {
612 return Err(ApiError::InvalidHandle(Some(
613 "Nested subdomains are not allowed. Use a simple handle without dots.".into(),
614 )));
615 }
616 if short_part.len() < 3 {
617 return Err(ApiError::InvalidHandle(Some("Handle too short".into())));
618 }
619 if short_part.len() > 18 {
620 return Err(ApiError::InvalidHandle(Some("Handle too long".into())));
621 }
622 full_handle
623 .parse()
624 .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?
625 } else {
626 let handle: Handle = new_handle
627 .parse()
628 .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?;
629 if new_handle == current_handle {
630 if let Err(e) =
631 tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
632 {
633 warn!("Failed to sequence identity event for handle update: {}", e);
634 }
635 return Ok(Json(EmptyResponse {}));
636 }
637 match tranquil_pds::handle::verify_handle_ownership(&handle, &did).await {
638 Ok(()) => {}
639 Err(tranquil_pds::handle::HandleResolutionError::NotFound) => {
640 return Err(ApiError::HandleNotAvailable(None));
641 }
642 Err(tranquil_pds::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
643 return Err(ApiError::HandleNotAvailable(Some(format!(
644 "Handle points to different DID. Expected {}, got {}",
645 expected, actual
646 ))));
647 }
648 Err(e) => {
649 warn!("Handle verification failed: {}", e);
650 return Err(ApiError::HandleNotAvailable(Some(format!(
651 "Handle verification failed: {}",
652 e
653 ))));
654 }
655 }
656 handle
657 };
658 let handle_exists = state
659 .repos
660 .user
661 .check_handle_exists(&handle, user_id)
662 .await
663 .log_db_err("checking handle existence")?;
664 if handle_exists {
665 return Err(ApiError::HandleTaken);
666 }
667 state
668 .repos
669 .user
670 .update_handle(user_id, &handle)
671 .await
672 .map_err(|e| {
673 error!("DB error updating handle: {:?}", e);
674 ApiError::InternalError(None)
675 })?;
676
677 if !current_handle.is_empty() {
678 let _ = state
679 .cache
680 .delete(&tranquil_pds::cache_keys::handle_key(¤t_handle))
681 .await;
682 }
683 let _ = state
684 .cache
685 .delete(&tranquil_pds::cache_keys::handle_key(&handle))
686 .await;
687 if let Err(e) =
688 tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
689 {
690 warn!("Failed to sequence identity event for handle update: {}", e);
691 }
692 if let Err(e) = update_plc_handle(&state, &did, &handle).await {
693 warn!("Failed to update PLC handle: {}", e);
694 }
695 Ok(Json(EmptyResponse {}))
696}
697
698pub async fn update_plc_handle(
699 state: &AppState,
700 did: &tranquil_pds::types::Did,
701 new_handle: &Handle,
702) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
703 if !did.as_str().starts_with("did:plc:") {
704 return Ok(());
705 }
706 let user_row = match state.repos.user.get_user_with_key_by_did(did).await? {
707 Some(r) => r,
708 None => return Ok(()),
709 };
710 let key_bytes =
711 tranquil_pds::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
712 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
713 let plc_client = state.plc_client();
714 let last_op = plc_client.get_last_op(did).await?;
715 let new_also_known_as = vec![format!("at://{}", new_handle)];
716 let update_op =
717 tranquil_pds::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
718 let signed_op = tranquil_pds::plc::sign_operation(&update_op, &signing_key)?;
719 plc_client.send_operation(did, &signed_op).await?;
720 Ok(())
721}
722
723pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
724 let host = match tranquil_pds::util::get_header_str(&headers, http::header::HOST) {
725 Some(h) => h,
726 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
727 };
728 let handle_str = host.split(':').next().unwrap_or(host);
729 let handle: Handle = match handle_str.parse() {
730 Ok(h) => h,
731 Err(_) => return (StatusCode::BAD_REQUEST, "Invalid handle format").into_response(),
732 };
733 let user = state.repos.user.get_by_handle(&handle).await;
734 match user {
735 Ok(Some(row)) => row.did.to_string().into_response(),
736 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
737 Err(e) => {
738 error!("DB error in well-known atproto-did: {:?}", e);
739 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
740 }
741 }
742}