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