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