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