Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
238
fork

Configure Feed

Select the types of activity you want to include in your feed.

Identity endpoint conformance vs ref

+367 -140
+99 -23
src/api/identity/did.rs
··· 511 511 let rotation_keys = if auth_user.did.starts_with("did:web:") { 512 512 vec![] 513 513 } else { 514 - vec![did_key.clone()] 514 + let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { 515 + Ok(key) => key, 516 + Err(_) => { 517 + warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"); 518 + did_key.clone() 519 + } 520 + }; 521 + vec![server_rotation_key] 515 522 }; 516 523 ( 517 524 StatusCode::OK, ··· 559 566 return e; 560 567 } 561 568 let did = auth_user.did; 562 - let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 563 - .fetch_optional(&state.db) 569 + if !state 570 + .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 564 571 .await 565 572 { 566 - Ok(Some(id)) => id, 573 + return ( 574 + StatusCode::TOO_MANY_REQUESTS, 575 + Json(json!({"error": "RateLimitExceeded", "message": "Too many handle updates. Try again later."})), 576 + ) 577 + .into_response(); 578 + } 579 + if !state 580 + .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 581 + .await 582 + { 583 + return ( 584 + StatusCode::TOO_MANY_REQUESTS, 585 + Json(json!({"error": "RateLimitExceeded", "message": "Daily handle update limit exceeded."})), 586 + ) 587 + .into_response(); 588 + } 589 + let user_row = match sqlx::query!( 590 + "SELECT id, handle FROM users WHERE did = $1", 591 + did 592 + ) 593 + .fetch_optional(&state.db) 594 + .await 595 + { 596 + Ok(Some(row)) => row, 567 597 _ => return ApiError::InternalError.into_response(), 568 598 }; 569 - let new_handle = input.handle.trim(); 599 + let user_id = user_row.id; 600 + let current_handle = user_row.handle; 601 + let new_handle = input.handle.trim().to_ascii_lowercase(); 570 602 if new_handle.is_empty() { 571 603 return ApiError::InvalidRequest("handle is required".into()).into_response(); 572 604 } 573 605 if !new_handle 574 606 .chars() 575 - .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 607 + .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 576 608 { 577 609 return ( 578 610 StatusCode::BAD_REQUEST, ··· 582 614 ) 583 615 .into_response(); 584 616 } 585 - if crate::moderation::has_explicit_slur(new_handle) { 617 + for segment in new_handle.split('.') { 618 + if segment.is_empty() { 619 + return ( 620 + StatusCode::BAD_REQUEST, 621 + Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})), 622 + ) 623 + .into_response(); 624 + } 625 + if segment.starts_with('-') || segment.ends_with('-') { 626 + return ( 627 + StatusCode::BAD_REQUEST, 628 + Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})), 629 + ) 630 + .into_response(); 631 + } 632 + } 633 + if crate::moderation::has_explicit_slur(&new_handle) { 586 634 return ( 587 635 StatusCode::BAD_REQUEST, 588 636 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), ··· 591 639 } 592 640 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 593 641 let suffix = format!(".{}", hostname); 594 - let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname); 642 + let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname); 595 643 let handle = if is_service_domain { 596 644 let short_part = if new_handle.ends_with(&suffix) { 597 - new_handle.strip_suffix(&suffix).unwrap_or(new_handle) 645 + new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 598 646 } else { 599 - new_handle 647 + &new_handle 600 648 }; 649 + let full_handle = if new_handle.ends_with(&suffix) { 650 + new_handle.clone() 651 + } else { 652 + format!("{}.{}", new_handle, hostname) 653 + }; 654 + if full_handle == current_handle { 655 + if let Err(e) = 656 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)) 657 + .await 658 + { 659 + warn!("Failed to sequence identity event for handle update: {}", e); 660 + } 661 + return (StatusCode::OK, Json(json!({}))).into_response(); 662 + } 601 663 if short_part.contains('.') { 602 664 return ( 603 665 StatusCode::BAD_REQUEST, ··· 608 670 ) 609 671 .into_response(); 610 672 } 611 - if new_handle.ends_with(&suffix) { 612 - new_handle.to_string() 613 - } else { 614 - format!("{}.{}", new_handle, hostname) 673 + if short_part.len() < 3 { 674 + return ( 675 + StatusCode::BAD_REQUEST, 676 + Json(json!({"error": "InvalidHandle", "message": "Handle too short"})), 677 + ) 678 + .into_response(); 679 + } 680 + if short_part.len() > 18 { 681 + return ( 682 + StatusCode::BAD_REQUEST, 683 + Json(json!({"error": "InvalidHandle", "message": "Handle too long"})), 684 + ) 685 + .into_response(); 615 686 } 687 + full_handle 616 688 } else { 617 - match crate::handle::verify_handle_ownership(new_handle, &did).await { 689 + if new_handle == current_handle { 690 + if let Err(e) = 691 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle)) 692 + .await 693 + { 694 + warn!("Failed to sequence identity event for handle update: {}", e); 695 + } 696 + return (StatusCode::OK, Json(json!({}))).into_response(); 697 + } 698 + match crate::handle::verify_handle_ownership(&new_handle, &did).await { 618 699 Ok(()) => {} 619 700 Err(crate::handle::HandleResolutionError::NotFound) => { 620 701 return ( ··· 649 730 .into_response(); 650 731 } 651 732 } 652 - new_handle.to_string() 733 + new_handle.clone() 653 734 }; 654 - let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id) 655 - .fetch_optional(&state.db) 656 - .await 657 - .ok() 658 - .flatten(); 659 735 let existing = sqlx::query!( 660 736 "SELECT id FROM users WHERE handle = $1 AND id != $2", 661 737 handle, ··· 679 755 .await; 680 756 match result { 681 757 Ok(_) => { 682 - if let Some(old) = old_handle { 683 - let _ = state.cache.delete(&format!("handle:{}", old)).await; 758 + if !current_handle.is_empty() { 759 + let _ = state.cache.delete(&format!("handle:{}", current_handle)).await; 684 760 } 685 761 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 686 762 if let Err(e) =
+28 -65
src/api/identity/plc/submit.rs
··· 23 23 headers: axum::http::HeaderMap, 24 24 Json(input): Json<SubmitPlcOperationInput>, 25 25 ) -> Response { 26 - info!("[MIGRATION] submitPlcOperation called"); 27 26 let bearer = match crate::auth::extract_bearer_token_from_header( 28 27 headers.get("Authorization").and_then(|h| h.to_str().ok()), 29 28 ) { 30 29 Some(t) => t, 31 30 None => { 32 - info!("[MIGRATION] submitPlcOperation: No bearer token"); 33 31 return ApiError::AuthenticationRequired.into_response(); 34 32 } 35 33 }; ··· 37 35 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await { 38 36 Ok(user) => user, 39 37 Err(e) => { 40 - info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e); 41 38 return ApiError::from(e).into_response(); 42 39 } 43 40 }; 44 - info!( 45 - "[MIGRATION] submitPlcOperation: Authenticated user did={}", 46 - auth_user.did 47 - ); 48 41 if let Err(e) = crate::auth::scope_check::check_identity_scope( 49 42 auth_user.is_oauth, 50 43 auth_user.scope.as_deref(), 51 44 crate::oauth::scopes::IdentityAttr::Wildcard, 52 45 ) { 53 - info!("[MIGRATION] submitPlcOperation: Scope check failed"); 54 46 return e; 55 47 } 56 48 let did = &auth_user.did; ··· 67 59 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 68 60 let public_url = format!("https://{}", hostname); 69 61 let user = match sqlx::query!( 70 - "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 62 + "SELECT id, handle FROM users WHERE did = $1", 71 63 did 72 64 ) 73 65 .fetch_optional(&state.db) ··· 82 74 .into_response(); 83 75 } 84 76 }; 85 - let is_migration = user.deactivated_at.is_some(); 86 77 let key_row = match sqlx::query!( 87 78 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 88 79 user.id ··· 123 114 } 124 115 }; 125 116 let user_did_key = signing_key_to_did_key(&signing_key); 126 - if !is_migration && let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) 127 - { 128 - let server_rotation_key = 129 - std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); 117 + let server_rotation_key = 118 + std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); 119 + if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) { 130 120 let has_server_key = rotation_keys 131 121 .iter() 132 122 .any(|k| k.as_str() == Some(&server_rotation_key)); ··· 167 157 .into_response(); 168 158 } 169 159 } 170 - if !is_migration { 171 - if let Some(verification_methods) = 172 - op.get("verificationMethods").and_then(|v| v.as_object()) 173 - && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 174 - && atproto_key != user_did_key 175 - { 176 - return ( 177 - StatusCode::BAD_REQUEST, 178 - Json(json!({ 179 - "error": "InvalidRequest", 180 - "message": "Incorrect signing key in verificationMethods" 181 - })), 182 - ) 183 - .into_response(); 184 - } 160 + if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 161 + && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 162 + && atproto_key != user_did_key 163 + { 164 + return ( 165 + StatusCode::BAD_REQUEST, 166 + Json(json!({ 167 + "error": "InvalidRequest", 168 + "message": "Incorrect signing key in verificationMethods" 169 + })), 170 + ) 171 + .into_response(); 172 + } 173 + if !user.handle.is_empty() { 185 174 if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) { 186 175 let expected_handle = format!("at://{}", user.handle); 187 176 let first_aka = also_known_as.first().and_then(|v| v.as_str()); ··· 200 189 let plc_client = PlcClient::new(None); 201 190 let operation_clone = input.operation.clone(); 202 191 let did_clone = did.clone(); 203 - info!( 204 - "[MIGRATION] submitPlcOperation: Sending operation to PLC directory for did={}", 205 - did 206 - ); 207 - let plc_start = std::time::Instant::now(); 208 192 let result: Result<(), CircuitBreakerError<PlcError>> = 209 193 with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 210 194 plc_client ··· 213 197 }) 214 198 .await; 215 199 match result { 216 - Ok(()) => { 217 - info!( 218 - "[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}", 219 - plc_start.elapsed() 220 - ); 221 - } 200 + Ok(()) => {} 222 201 Err(CircuitBreakerError::CircuitOpen(e)) => { 223 - warn!( 224 - "[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}", 225 - e 226 - ); 202 + warn!("PLC directory circuit breaker open: {}", e); 227 203 return ( 228 204 StatusCode::SERVICE_UNAVAILABLE, 229 205 Json(json!({ ··· 234 210 .into_response(); 235 211 } 236 212 Err(CircuitBreakerError::OperationFailed(e)) => { 237 - error!( 238 - "[MIGRATION] submitPlcOperation: PLC operation failed: {:?}", 239 - e 240 - ); 213 + error!("PLC operation failed: {:?}", e); 241 214 return ( 242 215 StatusCode::BAD_GATEWAY, 243 216 Json(json!({ ··· 248 221 .into_response(); 249 222 } 250 223 } 251 - info!( 252 - "[MIGRATION] submitPlcOperation: Sequencing identity event for did={}", 253 - did 254 - ); 255 224 match sqlx::query!( 256 225 "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq", 257 226 did ··· 260 229 .await 261 230 { 262 231 Ok(row) => { 263 - info!( 264 - "[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}", 265 - row.seq 266 - ); 267 232 if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq)) 268 233 .execute(&state.db) 269 234 .await 270 235 { 271 - warn!( 272 - "[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}", 273 - e 274 - ); 236 + warn!("Failed to notify identity event: {:?}", e); 275 237 } 276 238 } 277 239 Err(e) => { 278 - warn!( 279 - "[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}", 280 - e 281 - ); 240 + warn!("Failed to sequence identity event: {:?}", e); 282 241 } 283 242 } 284 - info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did); 243 + let _ = state.cache.delete(&format!("handle:{}", user.handle)).await; 244 + if state.did_resolver.refresh_did(did).await.is_none() { 245 + warn!(did = %did, "Failed to refresh DID cache after PLC update"); 246 + } 247 + info!(did = %did, "PLC operation submitted successfully"); 285 248 (StatusCode::OK, Json(json!({}))).into_response() 286 249 }
+12 -12
src/api/validation.rs
··· 35 35 ), 36 36 Self::InvalidCharacters => write!( 37 37 f, 38 - "Handle contains invalid characters. Only alphanumeric, hyphens, and underscores are allowed" 38 + "Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed" 39 39 ), 40 40 Self::StartsWithInvalidChar => { 41 - write!(f, "Handle cannot start with a hyphen or underscore") 41 + write!(f, "Handle cannot start with a hyphen") 42 42 } 43 - Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen or underscore"), 43 + Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 44 44 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 45 45 Self::BannedWord => write!(f, "Inappropriate language in handle"), 46 46 } ··· 67 67 } 68 68 69 69 if let Some(first_char) = handle.chars().next() 70 - && (first_char == '-' || first_char == '_') 70 + && first_char == '-' 71 71 { 72 72 return Err(HandleValidationError::StartsWithInvalidChar); 73 73 } 74 74 75 75 if let Some(last_char) = handle.chars().last() 76 - && (last_char == '-' || last_char == '_') 76 + && last_char == '-' 77 77 { 78 78 return Err(HandleValidationError::EndsWithInvalidChar); 79 79 } 80 80 81 81 for c in handle.chars() { 82 - if !c.is_ascii_alphanumeric() && c != '-' && c != '_' { 82 + if !c.is_ascii_alphanumeric() && c != '-' { 83 83 return Err(HandleValidationError::InvalidCharacters); 84 84 } 85 85 } ··· 151 151 Ok("user-name".to_string()) 152 152 ); 153 153 assert_eq!( 154 - validate_short_handle("user_name"), 155 - Ok("user_name".to_string()) 156 - ); 157 - assert_eq!( 158 154 validate_short_handle("UPPERCASE"), 159 155 Ok("uppercase".to_string()) 160 156 ); ··· 194 190 ); 195 191 assert_eq!( 196 192 validate_short_handle("_starts"), 197 - Err(HandleValidationError::StartsWithInvalidChar) 193 + Err(HandleValidationError::InvalidCharacters) 198 194 ); 199 195 assert_eq!( 200 196 validate_short_handle("ends-"), ··· 202 198 ); 203 199 assert_eq!( 204 200 validate_short_handle("ends_"), 205 - Err(HandleValidationError::EndsWithInvalidChar) 201 + Err(HandleValidationError::InvalidCharacters) 202 + ); 203 + assert_eq!( 204 + validate_short_handle("user_name"), 205 + Err(HandleValidationError::InvalidCharacters) 206 206 ); 207 207 assert_eq!( 208 208 validate_short_handle("test@user"),
+8
src/appview/mod.rs
··· 110 110 Some(resolved) 111 111 } 112 112 113 + pub async fn refresh_did(&self, did: &str) -> Option<ResolvedService> { 114 + { 115 + let mut cache = self.did_cache.write().await; 116 + cache.remove(did); 117 + } 118 + self.resolve_did(did).await 119 + } 120 + 113 121 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 114 122 let did_doc = if did.starts_with("did:web:") { 115 123 self.resolve_did_web(did).await
+4
src/handle/mod.rs
··· 93 93 } 94 94 95 95 pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool { 96 + if !handle.contains('.') { 97 + return true; 98 + } 96 99 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS") 97 100 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect()) 98 101 .unwrap_or_else(|_| vec![hostname.to_string()]); ··· 115 118 fn test_is_service_domain_handle() { 116 119 assert!(is_service_domain_handle("user.example.com", "example.com")); 117 120 assert!(is_service_domain_handle("example.com", "example.com")); 121 + assert!(is_service_domain_handle("myhandle", "example.com")); 118 122 assert!(!is_service_domain_handle("user.other.com", "example.com")); 119 123 assert!(!is_service_domain_handle("myhandle.xyz", "example.com")); 120 124 }
+12
src/rate_limit.rs
··· 30 30 pub app_password: Arc<KeyedRateLimiter>, 31 31 pub email_update: Arc<KeyedRateLimiter>, 32 32 pub totp_verify: Arc<KeyedRateLimiter>, 33 + pub handle_update: Arc<KeyedRateLimiter>, 34 + pub handle_update_daily: Arc<KeyedRateLimiter>, 33 35 } 34 36 35 37 impl Default for RateLimiters { ··· 78 80 Quota::with_period(std::time::Duration::from_secs(60)) 79 81 .unwrap() 80 82 .allow_burst(NonZeroU32::new(5).unwrap()), 83 + )), 84 + handle_update: Arc::new(RateLimiter::keyed( 85 + Quota::with_period(std::time::Duration::from_secs(30)) 86 + .unwrap() 87 + .allow_burst(NonZeroU32::new(10).unwrap()), 88 + )), 89 + handle_update_daily: Arc::new(RateLimiter::keyed( 90 + Quota::with_period(std::time::Duration::from_secs(1728)) 91 + .unwrap() 92 + .allow_burst(NonZeroU32::new(50).unwrap()), 81 93 )), 82 94 } 83 95 }
+8
src/state.rs
··· 37 37 AppPassword, 38 38 EmailUpdate, 39 39 TotpVerify, 40 + HandleUpdate, 41 + HandleUpdateDaily, 40 42 } 41 43 42 44 impl RateLimitKind { ··· 54 56 Self::AppPassword => "app_password", 55 57 Self::EmailUpdate => "email_update", 56 58 Self::TotpVerify => "totp_verify", 59 + Self::HandleUpdate => "handle_update", 60 + Self::HandleUpdateDaily => "handle_update_daily", 57 61 } 58 62 } 59 63 ··· 71 75 Self::AppPassword => (10, 60_000), 72 76 Self::EmailUpdate => (5, 3_600_000), 73 77 Self::TotpVerify => (5, 300_000), 78 + Self::HandleUpdate => (10, 300_000), 79 + Self::HandleUpdateDaily => (50, 86_400_000), 74 80 } 75 81 } 76 82 } ··· 191 197 RateLimitKind::AppPassword => &self.rate_limiters.app_password, 192 198 RateLimitKind::EmailUpdate => &self.rate_limiters.email_update, 193 199 RateLimitKind::TotpVerify => &self.rate_limiters.totp_verify, 200 + RateLimitKind::HandleUpdate => &self.rate_limiters.handle_update, 201 + RateLimitKind::HandleUpdateDaily => &self.rate_limiters.handle_update_daily, 194 202 }; 195 203 196 204 let ok = limiter.check_key(&client_ip.to_string()).is_ok();
+1 -1
tests/common/mod.rs
··· 430 430 if attempt > 0 { 431 431 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 432 432 } 433 - let handle = format!("user_{}", uuid::Uuid::new_v4()); 433 + let handle = format!("user-{}", uuid::Uuid::new_v4()); 434 434 let payload = json!({ 435 435 "handle": handle, 436 436 "email": format!("{}@example.com", handle),
+7 -7
tests/did_web.rs
··· 11 11 #[tokio::test] 12 12 async fn test_create_self_hosted_did_web() { 13 13 let client = client(); 14 - let handle = format!("selfweb_{}", uuid::Uuid::new_v4()); 14 + let handle = format!("selfweb-{}", uuid::Uuid::new_v4()); 15 15 let payload = json!({ 16 16 "handle": handle, 17 17 "email": format!("{}@example.com", handle), ··· 98 98 let mock_uri = mock_server.uri(); 99 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("extweb_{}", uuid::Uuid::new_v4()); 101 + let handle = format!("extweb-{}", uuid::Uuid::new_v4()); 102 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 103 104 104 let reserve_res = client ··· 180 180 #[tokio::test] 181 181 async fn test_plc_operations_blocked_for_did_web() { 182 182 let client = client(); 183 - let handle = format!("plcblock_{}", uuid::Uuid::new_v4()); 183 + let handle = format!("plcblock-{}", uuid::Uuid::new_v4()); 184 184 let payload = json!({ 185 185 "handle": handle, 186 186 "email": format!("{}@example.com", handle), ··· 245 245 #[tokio::test] 246 246 async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { 247 247 let client = client(); 248 - let handle = format!("creds_{}", uuid::Uuid::new_v4()); 248 + let handle = format!("creds-{}", uuid::Uuid::new_v4()); 249 249 let payload = json!({ 250 250 "handle": handle, 251 251 "email": format!("{}@example.com", handle), ··· 294 294 #[tokio::test] 295 295 async fn test_did_plc_still_works_with_did_type_param() { 296 296 let client = client(); 297 - let handle = format!("plctype_{}", uuid::Uuid::new_v4()); 297 + let handle = format!("plctype-{}", uuid::Uuid::new_v4()); 298 298 let payload = json!({ 299 299 "handle": handle, 300 300 "email": format!("{}@example.com", handle), ··· 323 323 #[tokio::test] 324 324 async fn test_external_did_web_requires_did_field() { 325 325 let client = client(); 326 - let handle = format!("nodid_{}", uuid::Uuid::new_v4()); 326 + let handle = format!("nodid-{}", uuid::Uuid::new_v4()); 327 327 let payload = json!({ 328 328 "handle": handle, 329 329 "email": format!("{}@example.com", handle), ··· 392 392 mock_addr.replace(":", "%3A"), 393 393 unique_id 394 394 ); 395 - let handle = format!("byod_{}", uuid::Uuid::new_v4()); 395 + let handle = format!("byod-{}", uuid::Uuid::new_v4()); 396 396 let pds_endpoint = base_url().await.replace("http://", "https://"); 397 397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); 398 398
+13 -13
tests/email_update.rs
··· 67 67 let client = common::client(); 68 68 let base_url = common::base_url().await; 69 69 let pool = get_pool().await; 70 - let handle = format!("emailup_{}", uuid::Uuid::new_v4()); 70 + let handle = format!("emailup-{}", uuid::Uuid::new_v4()); 71 71 let email = format!("{}@example.com", handle); 72 72 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 73 73 let new_email = format!("new_{}@example.com", handle); ··· 108 108 async fn test_request_email_update_taken_email() { 109 109 let client = common::client(); 110 110 let base_url = common::base_url().await; 111 - let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4()); 111 + let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4()); 112 112 let email1 = format!("{}@example.com", handle1); 113 113 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 114 - let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4()); 114 + let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4()); 115 115 let email2 = format!("{}@example.com", handle2); 116 116 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 117 117 let res = client ··· 133 133 async fn test_confirm_email_invalid_token() { 134 134 let client = common::client(); 135 135 let base_url = common::base_url().await; 136 - let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4()); 136 + let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4()); 137 137 let email = format!("{}@example.com", handle); 138 138 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 139 139 let new_email = format!("new_{}@example.com", handle); ··· 168 168 let client = common::client(); 169 169 let base_url = common::base_url().await; 170 170 let pool = get_pool().await; 171 - let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4()); 171 + let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4()); 172 172 let email = format!("{}@example.com", handle); 173 173 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 174 174 let new_email = format!("new_{}@example.com", handle); ··· 205 205 async fn test_update_email_requires_token() { 206 206 let client = common::client(); 207 207 let base_url = common::base_url().await; 208 - let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4()); 208 + let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); 209 209 let email = format!("{}@example.com", handle); 210 210 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 211 211 let new_email = format!("direct_{}@example.com", handle); ··· 225 225 async fn test_update_email_same_email_noop() { 226 226 let client = common::client(); 227 227 let base_url = common::base_url().await; 228 - let handle = format!("emailup_same_{}", uuid::Uuid::new_v4()); 228 + let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); 229 229 let email = format!("{}@example.com", handle); 230 230 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 231 231 let res = client ··· 246 246 async fn test_update_email_requires_token_after_pending() { 247 247 let client = common::client(); 248 248 let base_url = common::base_url().await; 249 - let handle = format!("emailup_token_{}", uuid::Uuid::new_v4()); 249 + let handle = format!("emailup-token-{}", uuid::Uuid::new_v4()); 250 250 let email = format!("{}@example.com", handle); 251 251 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 252 252 let new_email = format!("pending_{}@example.com", handle); ··· 278 278 let client = common::client(); 279 279 let base_url = common::base_url().await; 280 280 let pool = get_pool().await; 281 - let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4()); 281 + let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4()); 282 282 let email = format!("{}@example.com", handle); 283 283 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 284 284 let new_email = format!("valid_{}@example.com", handle); ··· 316 316 async fn test_update_email_invalid_token() { 317 317 let client = common::client(); 318 318 let base_url = common::base_url().await; 319 - let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4()); 319 + let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); 320 320 let email = format!("{}@example.com", handle); 321 321 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 322 322 let new_email = format!("badtok_{}@example.com", handle); ··· 350 350 async fn test_update_email_already_taken() { 351 351 let client = common::client(); 352 352 let base_url = common::base_url().await; 353 - let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4()); 353 + let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); 354 354 let email1 = format!("{}@example.com", handle1); 355 355 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 356 - let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4()); 356 + let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); 357 357 let email2 = format!("{}@example.com", handle2); 358 358 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 359 359 let res = client ··· 394 394 async fn test_update_email_invalid_format() { 395 395 let client = common::client(); 396 396 let base_url = common::base_url().await; 397 - let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4()); 397 + let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); 398 398 let email = format!("{}@example.com", handle); 399 399 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 400 400 let res = client
+160 -4
tests/identity.rs
··· 8 8 #[tokio::test] 9 9 async fn test_resolve_handle_success() { 10 10 let client = client(); 11 - let short_handle = format!("resolvetest_{}", uuid::Uuid::new_v4()); 11 + let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4()); 12 12 let payload = json!({ 13 13 "handle": short_handle, 14 14 "email": format!("{}@example.com", short_handle), ··· 98 98 let mock_uri = mock_server.uri(); 99 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("webuser_{}", uuid::Uuid::new_v4()); 101 + let handle = format!("webuser-{}", uuid::Uuid::new_v4()); 102 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 103 104 104 let reserve_res = client ··· 183 183 #[tokio::test] 184 184 async fn test_create_account_duplicate_handle() { 185 185 let client = client(); 186 - let handle = format!("dupe_{}", uuid::Uuid::new_v4()); 186 + let handle = format!("dupe-{}", uuid::Uuid::new_v4()); 187 187 let email = format!("{}@example.com", handle); 188 188 let payload = json!({ 189 189 "handle": handle, ··· 220 220 let mock_server = MockServer::start().await; 221 221 let mock_uri = mock_server.uri(); 222 222 let mock_addr = mock_uri.trim_start_matches("http://"); 223 - let handle = format!("lifecycle_{}", uuid::Uuid::new_v4()); 223 + let handle = format!("lifecycle-{}", uuid::Uuid::new_v4()); 224 224 let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); 225 225 let email = format!("{}@test.com", handle); 226 226 let pds_endpoint = base_url().await.replace("http://", "https://"); ··· 378 378 let body: Value = res.json().await.expect("Response was not valid JSON"); 379 379 assert_eq!(body["error"], "AuthenticationRequired"); 380 380 } 381 + 382 + #[tokio::test] 383 + async fn test_update_handle_to_same() { 384 + let client = client(); 385 + let (access_jwt, _did) = create_account_and_login(&client).await; 386 + let session = client 387 + .get(format!( 388 + "{}/xrpc/com.atproto.server.getSession", 389 + base_url().await 390 + )) 391 + .bearer_auth(&access_jwt) 392 + .send() 393 + .await 394 + .expect("Failed to get session"); 395 + let session_body: Value = session.json().await.expect("Invalid JSON"); 396 + let current_handle = session_body["handle"].as_str().expect("No handle").to_string(); 397 + let short_handle = current_handle.split('.').next().unwrap_or(&current_handle); 398 + let res = client 399 + .post(format!( 400 + "{}/xrpc/com.atproto.identity.updateHandle", 401 + base_url().await 402 + )) 403 + .bearer_auth(&access_jwt) 404 + .json(&json!({ "handle": short_handle })) 405 + .send() 406 + .await 407 + .expect("Failed to send request"); 408 + assert_eq!(res.status(), StatusCode::OK); 409 + } 410 + 411 + #[tokio::test] 412 + async fn test_update_handle_no_auth() { 413 + let client = client(); 414 + let res = client 415 + .post(format!( 416 + "{}/xrpc/com.atproto.identity.updateHandle", 417 + base_url().await 418 + )) 419 + .json(&json!({ "handle": "newhandle" })) 420 + .send() 421 + .await 422 + .expect("Failed to send request"); 423 + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 424 + let body: Value = res.json().await.expect("Response was not valid JSON"); 425 + assert_eq!(body["error"], "AuthenticationRequired"); 426 + } 427 + 428 + #[tokio::test] 429 + async fn test_update_handle_invalid_characters() { 430 + let client = client(); 431 + let (access_jwt, _did) = create_account_and_login(&client).await; 432 + let res = client 433 + .post(format!( 434 + "{}/xrpc/com.atproto.identity.updateHandle", 435 + base_url().await 436 + )) 437 + .bearer_auth(&access_jwt) 438 + .json(&json!({ "handle": "invalid@handle!" })) 439 + .send() 440 + .await 441 + .expect("Failed to send request"); 442 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 443 + let body: Value = res.json().await.expect("Response was not valid JSON"); 444 + assert_eq!(body["error"], "InvalidHandle"); 445 + } 446 + 447 + #[tokio::test] 448 + async fn test_update_handle_empty() { 449 + let client = client(); 450 + let (access_jwt, _did) = create_account_and_login(&client).await; 451 + let res = client 452 + .post(format!( 453 + "{}/xrpc/com.atproto.identity.updateHandle", 454 + base_url().await 455 + )) 456 + .bearer_auth(&access_jwt) 457 + .json(&json!({ "handle": "" })) 458 + .send() 459 + .await 460 + .expect("Failed to send request"); 461 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 462 + let body: Value = res.json().await.expect("Response was not valid JSON"); 463 + assert_eq!(body["error"], "InvalidRequest"); 464 + } 465 + 466 + #[tokio::test] 467 + async fn test_update_handle_taken() { 468 + let client = client(); 469 + let (access_jwt1, _did1) = create_account_and_login(&client).await; 470 + let (access_jwt2, _did2) = create_account_and_login(&client).await; 471 + let short_handle = format!("taken{}", &uuid::Uuid::new_v4().to_string()[..8]); 472 + let update1 = client 473 + .post(format!( 474 + "{}/xrpc/com.atproto.identity.updateHandle", 475 + base_url().await 476 + )) 477 + .bearer_auth(&access_jwt1) 478 + .json(&json!({ "handle": short_handle })) 479 + .send() 480 + .await 481 + .expect("Failed to update handle"); 482 + assert_eq!(update1.status(), StatusCode::OK); 483 + let res = client 484 + .post(format!( 485 + "{}/xrpc/com.atproto.identity.updateHandle", 486 + base_url().await 487 + )) 488 + .bearer_auth(&access_jwt2) 489 + .json(&json!({ "handle": short_handle })) 490 + .send() 491 + .await 492 + .expect("Failed to send request"); 493 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 494 + let body: Value = res.json().await.expect("Response was not valid JSON"); 495 + assert_eq!(body["error"], "HandleTaken"); 496 + } 497 + 498 + #[tokio::test] 499 + async fn test_update_handle_too_short() { 500 + let client = client(); 501 + let (access_jwt, _did) = create_account_and_login(&client).await; 502 + let res = client 503 + .post(format!( 504 + "{}/xrpc/com.atproto.identity.updateHandle", 505 + base_url().await 506 + )) 507 + .bearer_auth(&access_jwt) 508 + .json(&json!({ "handle": "ab" })) 509 + .send() 510 + .await 511 + .expect("Failed to send request"); 512 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 513 + let body: Value = res.json().await.expect("Response was not valid JSON"); 514 + assert_eq!(body["error"], "InvalidHandle"); 515 + assert!(body["message"].as_str().unwrap().contains("short")); 516 + } 517 + 518 + #[tokio::test] 519 + async fn test_update_handle_too_long() { 520 + let client = client(); 521 + let (access_jwt, _did) = create_account_and_login(&client).await; 522 + let res = client 523 + .post(format!( 524 + "{}/xrpc/com.atproto.identity.updateHandle", 525 + base_url().await 526 + )) 527 + .bearer_auth(&access_jwt) 528 + .json(&json!({ "handle": "thishandleiswaytoolongforservicedomain" })) 529 + .send() 530 + .await 531 + .expect("Failed to send request"); 532 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 533 + let body: Value = res.json().await.expect("Response was not valid JSON"); 534 + assert_eq!(body["error"], "InvalidHandle"); 535 + assert!(body["message"].as_str().unwrap().contains("long")); 536 + }
+5 -5
tests/password_reset.rs
··· 19 19 let client = common::client(); 20 20 let base_url = common::base_url().await; 21 21 let pool = get_pool().await; 22 - let handle = format!("pwreset_{}", uuid::Uuid::new_v4()); 22 + let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); 23 23 let email = format!("{}@example.com", handle); 24 24 let payload = json!({ 25 25 "handle": handle, ··· 81 81 let client = common::client(); 82 82 let base_url = common::base_url().await; 83 83 let pool = get_pool().await; 84 - let handle = format!("pwreset2_{}", uuid::Uuid::new_v4()); 84 + let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); 85 85 let email = format!("{}@example.com", handle); 86 86 let old_password = "Oldpass123!"; 87 87 let new_password = "Newpass456!"; ··· 197 197 let client = common::client(); 198 198 let base_url = common::base_url().await; 199 199 let pool = get_pool().await; 200 - let handle = format!("pwreset3_{}", uuid::Uuid::new_v4()); 200 + let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); 201 201 let email = format!("{}@example.com", handle); 202 202 let payload = json!({ 203 203 "handle": handle, ··· 261 261 let client = common::client(); 262 262 let base_url = common::base_url().await; 263 263 let pool = get_pool().await; 264 - let handle = format!("pwreset4_{}", uuid::Uuid::new_v4()); 264 + let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); 265 265 let email = format!("{}@example.com", handle); 266 266 let payload = json!({ 267 267 "handle": handle, ··· 351 351 let pool = get_pool().await; 352 352 let client = common::client(); 353 353 let base_url = common::base_url().await; 354 - let handle = format!("pwreset5_{}", uuid::Uuid::new_v4()); 354 + let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); 355 355 let email = format!("{}@example.com", handle); 356 356 let payload = json!({ 357 357 "handle": handle,
+4 -4
tests/rate_limit.rs
··· 9 9 let client = client(); 10 10 let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await); 11 11 let payload = json!({ 12 - "identifier": "nonexistent_user_for_rate_limit_test", 12 + "identifier": "nonexistent-user-for-rate-limit-test", 13 13 "password": "wrongpassword" 14 14 }); 15 15 let mut rate_limited_count = 0; ··· 53 53 let mut success_count = 0; 54 54 for i in 0..8 { 55 55 let payload = json!({ 56 - "email": format!("ratelimit_test_{}@example.com", i) 56 + "email": format!("ratelimit-test_{}@example.com", i) 57 57 }); 58 58 let res = client 59 59 .post(&url) ··· 91 91 for i in 0..15 { 92 92 let unique_id = uuid::Uuid::new_v4(); 93 93 let payload = json!({ 94 - "handle": format!("ratelimit_{}_{}", i, unique_id), 95 - "email": format!("ratelimit_{}_{}@example.com", i, unique_id), 94 + "handle": format!("ratelimit-{}_{}", i, unique_id), 95 + "email": format!("ratelimit-{}_{}@example.com", i, unique_id), 96 96 "password": "Testpass123!" 97 97 }); 98 98 let res = client
+1 -1
tests/server.rs
··· 26 26 async fn test_account_and_session_lifecycle() { 27 27 let client = client(); 28 28 let base = base_url().await; 29 - let handle = format!("user_{}", uuid::Uuid::new_v4()); 29 + let handle = format!("user-{}", uuid::Uuid::new_v4()); 30 30 let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" }); 31 31 let create_res = client 32 32 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5 -5
tests/signing_key.rs
··· 174 174 assert_eq!(res.status(), StatusCode::OK); 175 175 let body: Value = res.json().await.unwrap(); 176 176 let signing_key = body["signingKey"].as_str().unwrap(); 177 - let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4()); 177 + let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); 178 178 let res = client 179 179 .post(format!( 180 180 "{}/xrpc/com.atproto.server.createAccount", ··· 212 212 async fn test_create_account_with_invalid_signing_key() { 213 213 let client = common::client(); 214 214 let base_url = common::base_url().await; 215 - let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4()); 215 + let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); 216 216 let res = client 217 217 .post(format!( 218 218 "{}/xrpc/com.atproto.server.createAccount", ··· 248 248 assert_eq!(res.status(), StatusCode::OK); 249 249 let body: Value = res.json().await.unwrap(); 250 250 let signing_key = body["signingKey"].as_str().unwrap(); 251 - let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4()); 251 + let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); 252 252 let res = client 253 253 .post(format!( 254 254 "{}/xrpc/com.atproto.server.createAccount", ··· 264 264 .await 265 265 .expect("Failed to create first account"); 266 266 assert_eq!(res.status(), StatusCode::OK); 267 - let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4()); 267 + let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); 268 268 let res = client 269 269 .post(format!( 270 270 "{}/xrpc/com.atproto.server.createAccount", ··· 301 301 assert_eq!(res.status(), StatusCode::OK); 302 302 let body: Value = res.json().await.unwrap(); 303 303 let signing_key = body["signingKey"].as_str().unwrap(); 304 - let handle = format!("token_test_user_{}", uuid::Uuid::new_v4()); 304 + let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); 305 305 let res = client 306 306 .post(format!( 307 307 "{}/xrpc/com.atproto.server.createAccount",