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