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::response(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::response(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::response(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(
631 "Too many handle updates. Try again later.".into(),
632 ))
633 .into_response();
634 }
635 if !state
636 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did)
637 .await
638 {
639 return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into()))
640 .into_response();
641 }
642 let user_row = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did.as_str())
643 .fetch_optional(&state.db)
644 .await
645 {
646 Ok(Some(row)) => row,
647 _ => return ApiError::InternalError(None).into_response(),
648 };
649 let user_id = user_row.id;
650 let current_handle = user_row.handle;
651 let new_handle = input.handle.trim().to_ascii_lowercase();
652 if new_handle.is_empty() {
653 return ApiError::InvalidRequest("handle is required".into()).into_response();
654 }
655 if !new_handle
656 .chars()
657 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
658 {
659 return ApiError::InvalidHandle(Some("Handle contains invalid characters".into()))
660 .into_response();
661 }
662 for segment in new_handle.split('.') {
663 if segment.is_empty() {
664 return ApiError::InvalidHandle(Some("Handle contains empty segment".into()))
665 .into_response();
666 }
667 if segment.starts_with('-') || segment.ends_with('-') {
668 return ApiError::InvalidHandle(Some(
669 "Handle segment cannot start or end with hyphen".into(),
670 ))
671 .into_response();
672 }
673 }
674 if crate::moderation::has_explicit_slur(&new_handle) {
675 return ApiError::InvalidHandle(Some("Inappropriate language in handle".into()))
676 .into_response();
677 }
678 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
679 let suffix = format!(".{}", hostname);
680 let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname);
681 let handle = if is_service_domain {
682 let short_part = if new_handle.ends_with(&suffix) {
683 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
684 } else {
685 &new_handle
686 };
687 let full_handle = if new_handle.ends_with(&suffix) {
688 new_handle.clone()
689 } else {
690 format!("{}.{}", new_handle, hostname)
691 };
692 if full_handle == current_handle {
693 if let Err(e) =
694 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
695 .await
696 {
697 warn!("Failed to sequence identity event for handle update: {}", e);
698 }
699 return EmptyResponse::ok().into_response();
700 }
701 if short_part.contains('.') {
702 return ApiError::InvalidHandle(Some(
703 "Nested subdomains are not allowed. Use a simple handle without dots.".into(),
704 ))
705 .into_response();
706 }
707 if short_part.len() < 3 {
708 return ApiError::InvalidHandle(Some("Handle too short".into())).into_response();
709 }
710 if short_part.len() > 18 {
711 return ApiError::InvalidHandle(Some("Handle too long".into())).into_response();
712 }
713 full_handle
714 } else {
715 if new_handle == current_handle {
716 if let Err(e) =
717 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle))
718 .await
719 {
720 warn!("Failed to sequence identity event for handle update: {}", e);
721 }
722 return EmptyResponse::ok().into_response();
723 }
724 match crate::handle::verify_handle_ownership(&new_handle, &did).await {
725 Ok(()) => {}
726 Err(crate::handle::HandleResolutionError::NotFound) => {
727 return ApiError::HandleNotAvailable(None).into_response();
728 }
729 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
730 return ApiError::HandleNotAvailable(Some(format!(
731 "Handle points to different DID. Expected {}, got {}",
732 expected, actual
733 )))
734 .into_response();
735 }
736 Err(e) => {
737 warn!("Handle verification failed: {}", e);
738 return ApiError::HandleNotAvailable(Some(format!(
739 "Handle verification failed: {}",
740 e
741 )))
742 .into_response();
743 }
744 }
745 new_handle.clone()
746 };
747 let existing = sqlx::query!(
748 "SELECT id FROM users WHERE handle = $1 AND id != $2",
749 handle,
750 user_id
751 )
752 .fetch_optional(&state.db)
753 .await;
754 if let Ok(Some(_)) = existing {
755 return ApiError::HandleTaken.into_response();
756 }
757 let result = sqlx::query!(
758 "UPDATE users SET handle = $1 WHERE id = $2",
759 handle,
760 user_id
761 )
762 .execute(&state.db)
763 .await;
764 match result {
765 Ok(_) => {
766 if !current_handle.is_empty() {
767 let _ = state
768 .cache
769 .delete(&format!("handle:{}", current_handle))
770 .await;
771 }
772 let _ = state.cache.delete(&format!("handle:{}", handle)).await;
773 if let Err(e) =
774 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
775 {
776 warn!("Failed to sequence identity event for handle update: {}", e);
777 }
778 if let Err(e) = update_plc_handle(&state, &did, &handle).await {
779 warn!("Failed to update PLC handle: {}", e);
780 }
781 EmptyResponse::ok().into_response()
782 }
783 Err(e) => {
784 error!("DB error updating handle: {:?}", e);
785 ApiError::InternalError(None).into_response()
786 }
787 }
788}
789
790pub async fn update_plc_handle(
791 state: &AppState,
792 did: &str,
793 new_handle: &str,
794) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
795 if !did.starts_with("did:plc:") {
796 return Ok(());
797 }
798 let user_row = sqlx::query!(
799 r#"SELECT u.id, uk.key_bytes, uk.encryption_version
800 FROM users u
801 JOIN user_keys uk ON u.id = uk.user_id
802 WHERE u.did = $1"#,
803 did
804 )
805 .fetch_optional(&state.db)
806 .await?;
807 let user_row = match user_row {
808 Some(r) => r,
809 None => return Ok(()),
810 };
811 let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
812 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
813 let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
814 let last_op = plc_client.get_last_op(did).await?;
815 let new_also_known_as = vec![format!("at://{}", new_handle)];
816 let update_op =
817 crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
818 let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
819 plc_client.send_operation(did, &signed_op).await?;
820 Ok(())
821}
822
823pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
824 let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
825 Some(h) => h,
826 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
827 };
828 let handle = host.split(':').next().unwrap_or(host);
829 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle)
830 .fetch_optional(&state.db)
831 .await;
832 match user {
833 Ok(Some(row)) => row.did.into_response(),
834 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
835 Err(e) => {
836 error!("DB error in well-known atproto-did: {:?}", e);
837 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
838 }
839 }
840}