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