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(ref ovr) = overrides {
195 if let Ok(parsed) =
196 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone())
197 {
198 if !parsed.is_empty() {
199 let also_known_as = if !ovr.also_known_as.is_empty() {
200 ovr.also_known_as.clone()
201 } else {
202 vec![format!("at://{}", full_handle)]
203 };
204
205 return Json(json!({
206 "@context": [
207 "https://www.w3.org/ns/did/v1",
208 "https://w3id.org/security/multikey/v1",
209 "https://w3id.org/security/suites/secp256k1-2019/v1"
210 ],
211 "id": did,
212 "alsoKnownAs": also_known_as,
213 "verificationMethod": parsed.iter().map(|m| json!({
214 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }),
215 "type": m.method_type,
216 "controller": did,
217 "publicKeyMultibase": m.public_key_multibase
218 })).collect::<Vec<_>>(),
219 "service": [{
220 "id": "#atproto_pds",
221 "type": "AtprotoPersonalDataServer",
222 "serviceEndpoint": service_endpoint
223 }]
224 }))
225 .into_response();
226 }
227 }
228 }
229
230 let key_row = sqlx::query!(
231 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
232 user_id
233 )
234 .fetch_optional(&state.db)
235 .await;
236 let key_bytes: Vec<u8> = match key_row {
237 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
238 Ok(k) => k,
239 Err(_) => {
240 return (
241 StatusCode::INTERNAL_SERVER_ERROR,
242 Json(json!({"error": "InternalError"})),
243 )
244 .into_response();
245 }
246 },
247 _ => {
248 return (
249 StatusCode::INTERNAL_SERVER_ERROR,
250 Json(json!({"error": "InternalError"})),
251 )
252 .into_response();
253 }
254 };
255 let public_key_multibase = match get_public_key_multibase(&key_bytes) {
256 Ok(pk) => pk,
257 Err(e) => {
258 tracing::error!("Failed to generate public key multibase: {}", e);
259 return (
260 StatusCode::INTERNAL_SERVER_ERROR,
261 Json(json!({"error": "InternalError"})),
262 )
263 .into_response();
264 }
265 };
266
267 let also_known_as = if let Some(ref ovr) = overrides {
268 if !ovr.also_known_as.is_empty() {
269 ovr.also_known_as.clone()
270 } else {
271 vec![format!("at://{}", full_handle)]
272 }
273 } else {
274 vec![format!("at://{}", full_handle)]
275 };
276
277 Json(json!({
278 "@context": [
279 "https://www.w3.org/ns/did/v1",
280 "https://w3id.org/security/multikey/v1",
281 "https://w3id.org/security/suites/secp256k1-2019/v1"
282 ],
283 "id": did,
284 "alsoKnownAs": also_known_as,
285 "verificationMethod": [{
286 "id": format!("{}#atproto", did),
287 "type": "Multikey",
288 "controller": did,
289 "publicKeyMultibase": public_key_multibase
290 }],
291 "service": [{
292 "id": "#atproto_pds",
293 "type": "AtprotoPersonalDataServer",
294 "serviceEndpoint": service_endpoint
295 }]
296 }))
297 .into_response()
298}
299
300pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
301 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
302 let full_handle = format!("{}.{}", handle, hostname);
303 let user = sqlx::query!(
304 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1",
305 full_handle
306 )
307 .fetch_optional(&state.db)
308 .await;
309 let (user_id, did, migrated_to_pds) = match user {
310 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds),
311 Ok(None) => {
312 return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response();
313 }
314 Err(e) => {
315 error!("DB Error: {:?}", e);
316 return (
317 StatusCode::INTERNAL_SERVER_ERROR,
318 Json(json!({"error": "InternalError"})),
319 )
320 .into_response();
321 }
322 };
323 if !did.starts_with("did:web:") {
324 return (
325 StatusCode::NOT_FOUND,
326 Json(json!({"error": "NotFound", "message": "User is not did:web"})),
327 )
328 .into_response();
329 }
330 let encoded_hostname = hostname.replace(':', "%3A");
331 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle);
332 let subdomain_host = format!("{}.{}", handle, hostname);
333 let encoded_subdomain = subdomain_host.replace(':', "%3A");
334 let new_subdomain_format = format!("did:web:{}", encoded_subdomain);
335 if did != old_path_format && did != new_subdomain_format {
336 return (
337 StatusCode::NOT_FOUND,
338 Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})),
339 )
340 .into_response();
341 }
342
343 let overrides = sqlx::query!(
344 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1",
345 user_id
346 )
347 .fetch_optional(&state.db)
348 .await
349 .ok()
350 .flatten();
351
352 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
353
354 if let Some(ref ovr) = overrides {
355 if let Ok(parsed) =
356 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone())
357 {
358 if !parsed.is_empty() {
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 }
389
390 let key_row = sqlx::query!(
391 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
392 user_id
393 )
394 .fetch_optional(&state.db)
395 .await;
396 let key_bytes: Vec<u8> = match key_row {
397 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
398 Ok(k) => k,
399 Err(_) => {
400 return (
401 StatusCode::INTERNAL_SERVER_ERROR,
402 Json(json!({"error": "InternalError"})),
403 )
404 .into_response();
405 }
406 },
407 _ => {
408 return (
409 StatusCode::INTERNAL_SERVER_ERROR,
410 Json(json!({"error": "InternalError"})),
411 )
412 .into_response();
413 }
414 };
415 let public_key_multibase = match get_public_key_multibase(&key_bytes) {
416 Ok(pk) => pk,
417 Err(e) => {
418 tracing::error!("Failed to generate public key multibase: {}", e);
419 return (
420 StatusCode::INTERNAL_SERVER_ERROR,
421 Json(json!({"error": "InternalError"})),
422 )
423 .into_response();
424 }
425 };
426
427 let also_known_as = if let Some(ref ovr) = overrides {
428 if !ovr.also_known_as.is_empty() {
429 ovr.also_known_as.clone()
430 } else {
431 vec![format!("at://{}", full_handle)]
432 }
433 } else {
434 vec![format!("at://{}", full_handle)]
435 };
436
437 Json(json!({
438 "@context": [
439 "https://www.w3.org/ns/did/v1",
440 "https://w3id.org/security/multikey/v1",
441 "https://w3id.org/security/suites/secp256k1-2019/v1"
442 ],
443 "id": did,
444 "alsoKnownAs": also_known_as,
445 "verificationMethod": [{
446 "id": format!("{}#atproto", did),
447 "type": "Multikey",
448 "controller": did,
449 "publicKeyMultibase": public_key_multibase
450 }],
451 "service": [{
452 "id": "#atproto_pds",
453 "type": "AtprotoPersonalDataServer",
454 "serviceEndpoint": service_endpoint
455 }]
456 }))
457 .into_response()
458}
459
460pub async fn verify_did_web(
461 did: &str,
462 hostname: &str,
463 handle: &str,
464 expected_signing_key: Option<&str>,
465) -> Result<(), String> {
466 let subdomain_host = format!("{}.{}", handle, hostname);
467 let encoded_subdomain = subdomain_host.replace(':', "%3A");
468 let expected_subdomain_did = format!("did:web:{}", encoded_subdomain);
469 if did == expected_subdomain_did {
470 return Ok(());
471 }
472 let expected_prefix = if hostname.contains(':') {
473 format!("did:web:{}", hostname.replace(':', "%3A"))
474 } else {
475 format!("did:web:{}", hostname)
476 };
477 if did.starts_with(&expected_prefix) {
478 let suffix = &did[expected_prefix.len()..];
479 let expected_suffix = format!(":u:{}", handle);
480 if suffix == expected_suffix {
481 return Ok(());
482 } else {
483 return Err(format!(
484 "Invalid DID path for this PDS. Expected {}",
485 expected_suffix
486 ));
487 }
488 }
489 let expected_signing_key = expected_signing_key.ok_or_else(|| {
490 "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()
491 })?;
492 let parts: Vec<&str> = did.split(':').collect();
493 if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" {
494 return Err("Invalid did:web format".into());
495 }
496 let domain_segment = parts[2];
497 let domain = domain_segment.replace("%3A", ":");
498 let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") {
499 "http"
500 } else {
501 "https"
502 };
503 let url = if parts.len() == 3 {
504 format!("{}://{}/.well-known/did.json", scheme, domain)
505 } else {
506 let path = parts[3..].join("/");
507 format!("{}://{}/{}/did.json", scheme, domain, path)
508 };
509 let client = reqwest::Client::builder()
510 .timeout(std::time::Duration::from_secs(5))
511 .build()
512 .map_err(|e| format!("Failed to create client: {}", e))?;
513 let resp = client
514 .get(&url)
515 .send()
516 .await
517 .map_err(|e| format!("Failed to fetch DID doc: {}", e))?;
518 if !resp.status().is_success() {
519 return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status()));
520 }
521 let doc: serde_json::Value = resp
522 .json()
523 .await
524 .map_err(|e| format!("Failed to parse DID doc: {}", e))?;
525 let services = doc["service"]
526 .as_array()
527 .ok_or("No services found in DID doc")?;
528 let pds_endpoint = format!("https://{}", hostname);
529 let has_valid_service = services
530 .iter()
531 .any(|s| s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint);
532 if !has_valid_service {
533 return Err(format!(
534 "DID document does not list this PDS ({}) as AtprotoPersonalDataServer",
535 pds_endpoint
536 ));
537 }
538 let verification_methods = doc["verificationMethod"]
539 .as_array()
540 .ok_or("No verificationMethod found in DID doc")?;
541 let expected_multibase = expected_signing_key
542 .strip_prefix("did:key:")
543 .ok_or("Invalid signing key format")?;
544 let has_matching_key = verification_methods.iter().any(|vm| {
545 vm["publicKeyMultibase"]
546 .as_str()
547 .map(|pk| pk == expected_multibase)
548 .unwrap_or(false)
549 });
550 if !has_matching_key {
551 return Err(format!(
552 "DID document verification key does not match reserved signing key. Expected publicKeyMultibase: {}",
553 expected_multibase
554 ));
555 }
556 Ok(())
557}
558
559#[derive(serde::Serialize)]
560#[serde(rename_all = "camelCase")]
561pub struct GetRecommendedDidCredentialsOutput {
562 pub rotation_keys: Vec<String>,
563 pub also_known_as: Vec<String>,
564 pub verification_methods: VerificationMethods,
565 pub services: Services,
566}
567
568#[derive(serde::Serialize)]
569#[serde(rename_all = "camelCase")]
570pub struct VerificationMethods {
571 pub atproto: String,
572}
573
574#[derive(serde::Serialize)]
575pub struct Services {
576 pub atproto_pds: AtprotoPds,
577}
578
579#[derive(serde::Serialize)]
580#[serde(rename_all = "camelCase")]
581pub struct AtprotoPds {
582 #[serde(rename = "type")]
583 pub service_type: String,
584 pub endpoint: String,
585}
586
587pub async fn get_recommended_did_credentials(
588 State(state): State<AppState>,
589 headers: axum::http::HeaderMap,
590) -> Response {
591 let token = match crate::auth::extract_bearer_token_from_header(
592 headers.get("Authorization").and_then(|h| h.to_str().ok()),
593 ) {
594 Some(t) => t,
595 None => {
596 return (
597 StatusCode::UNAUTHORIZED,
598 Json(json!({"error": "AuthenticationRequired"})),
599 )
600 .into_response();
601 }
602 };
603 let auth_user =
604 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
605 Ok(user) => user,
606 Err(e) => return ApiError::from(e).into_response(),
607 };
608 let user = match sqlx::query!(
609 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1",
610 auth_user.did
611 )
612 .fetch_optional(&state.db)
613 .await
614 {
615 Ok(Some(row)) => row,
616 _ => return ApiError::InternalError.into_response(),
617 };
618 let key_bytes = match auth_user.key_bytes {
619 Some(kb) => kb,
620 None => {
621 return ApiError::AuthenticationFailedMsg(
622 "OAuth tokens cannot get DID credentials".into(),
623 )
624 .into_response();
625 }
626 };
627 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
628 let pds_endpoint = format!("https://{}", hostname);
629 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) {
630 Ok(k) => k,
631 Err(_) => return ApiError::InternalError.into_response(),
632 };
633 let did_key = signing_key_to_did_key(&signing_key);
634 let rotation_keys = if auth_user.did.starts_with("did:web:") {
635 vec![]
636 } else {
637 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
638 Ok(key) => key,
639 Err(_) => {
640 warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation");
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!(
713 "SELECT id, handle FROM users WHERE did = $1",
714 did
715 )
716 .fetch_optional(&state.db)
717 .await
718 {
719 Ok(Some(row)) => row,
720 _ => return ApiError::InternalError.into_response(),
721 };
722 let user_id = user_row.id;
723 let current_handle = user_row.handle;
724 let new_handle = input.handle.trim().to_ascii_lowercase();
725 if new_handle.is_empty() {
726 return ApiError::InvalidRequest("handle is required".into()).into_response();
727 }
728 if !new_handle
729 .chars()
730 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
731 {
732 return (
733 StatusCode::BAD_REQUEST,
734 Json(
735 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
736 ),
737 )
738 .into_response();
739 }
740 for segment in new_handle.split('.') {
741 if segment.is_empty() {
742 return (
743 StatusCode::BAD_REQUEST,
744 Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})),
745 )
746 .into_response();
747 }
748 if segment.starts_with('-') || segment.ends_with('-') {
749 return (
750 StatusCode::BAD_REQUEST,
751 Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})),
752 )
753 .into_response();
754 }
755 }
756 if crate::moderation::has_explicit_slur(&new_handle) {
757 return (
758 StatusCode::BAD_REQUEST,
759 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})),
760 )
761 .into_response();
762 }
763 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
764 let suffix = format!(".{}", hostname);
765 let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname);
766 let handle = if is_service_domain {
767 let short_part = if new_handle.ends_with(&suffix) {
768 new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
769 } else {
770 &new_handle
771 };
772 let full_handle = if new_handle.ends_with(&suffix) {
773 new_handle.clone()
774 } else {
775 format!("{}.{}", new_handle, hostname)
776 };
777 if full_handle == current_handle {
778 if let Err(e) =
779 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
780 .await
781 {
782 warn!("Failed to sequence identity event for handle update: {}", e);
783 }
784 return (StatusCode::OK, Json(json!({}))).into_response();
785 }
786 if short_part.contains('.') {
787 return (
788 StatusCode::BAD_REQUEST,
789 Json(json!({
790 "error": "InvalidHandle",
791 "message": "Nested subdomains are not allowed. Use a simple handle without dots."
792 })),
793 )
794 .into_response();
795 }
796 if short_part.len() < 3 {
797 return (
798 StatusCode::BAD_REQUEST,
799 Json(json!({"error": "InvalidHandle", "message": "Handle too short"})),
800 )
801 .into_response();
802 }
803 if short_part.len() > 18 {
804 return (
805 StatusCode::BAD_REQUEST,
806 Json(json!({"error": "InvalidHandle", "message": "Handle too long"})),
807 )
808 .into_response();
809 }
810 full_handle
811 } else {
812 if new_handle == current_handle {
813 if let Err(e) =
814 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle))
815 .await
816 {
817 warn!("Failed to sequence identity event for handle update: {}", e);
818 }
819 return (StatusCode::OK, Json(json!({}))).into_response();
820 }
821 match crate::handle::verify_handle_ownership(&new_handle, &did).await {
822 Ok(()) => {}
823 Err(crate::handle::HandleResolutionError::NotFound) => {
824 return (
825 StatusCode::BAD_REQUEST,
826 Json(json!({
827 "error": "HandleNotAvailable",
828 "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did",
829 "handle": new_handle
830 })),
831 )
832 .into_response();
833 }
834 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
835 return (
836 StatusCode::BAD_REQUEST,
837 Json(json!({
838 "error": "HandleNotAvailable",
839 "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual)
840 })),
841 )
842 .into_response();
843 }
844 Err(e) => {
845 warn!("Handle verification failed: {}", e);
846 return (
847 StatusCode::BAD_REQUEST,
848 Json(json!({
849 "error": "HandleNotAvailable",
850 "message": format!("Handle verification failed: {}", e)
851 })),
852 )
853 .into_response();
854 }
855 }
856 new_handle.clone()
857 };
858 let existing = sqlx::query!(
859 "SELECT id FROM users WHERE handle = $1 AND id != $2",
860 handle,
861 user_id
862 )
863 .fetch_optional(&state.db)
864 .await;
865 if let Ok(Some(_)) = existing {
866 return (
867 StatusCode::BAD_REQUEST,
868 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
869 )
870 .into_response();
871 }
872 let result = sqlx::query!(
873 "UPDATE users SET handle = $1 WHERE id = $2",
874 handle,
875 user_id
876 )
877 .execute(&state.db)
878 .await;
879 match result {
880 Ok(_) => {
881 if !current_handle.is_empty() {
882 let _ = state.cache.delete(&format!("handle:{}", current_handle)).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}