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.

Reserved handles & misc fixes

+1690 -187
+1
.gitignore
··· 5 5 reference-pds-hailey/ 6 6 reference-pds-bsky/ 7 7 reference-relay-indigo/ 8 + pds-moover/ 8 9 # Frontend build artifacts 9 10 frontend/node_modules/ 10 11 frontend/dist/
+56 -13
src/api/actor/preferences.rs
··· 5 5 http::StatusCode, 6 6 response::{IntoResponse, Response}, 7 7 }; 8 + use chrono::{Datelike, NaiveDate, Utc}; 8 9 use serde::{Deserialize, Serialize}; 9 10 use serde_json::{Value, json}; 10 11 11 12 const APP_BSKY_NAMESPACE: &str = "app.bsky"; 12 13 const MAX_PREFERENCES_COUNT: usize = 100; 13 14 const MAX_PREFERENCE_SIZE: usize = 10_000; 15 + const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref"; 16 + const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref"; 17 + 18 + fn get_age_from_datestring(birth_date: &str) -> Option<i32> { 19 + let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?; 20 + let today = Utc::now().date_naive(); 21 + let mut age = today.year() - bday.year(); 22 + let m = today.month() as i32 - bday.month() as i32; 23 + if m < 0 || (m == 0 && today.day() < bday.day()) { 24 + age -= 1; 25 + } 26 + Some(age) 27 + } 14 28 15 29 #[derive(Serialize)] 16 30 pub struct GetPreferencesOutput { ··· 43 57 .into_response(); 44 58 } 45 59 }; 60 + let has_full_access = auth_user.permissions().has_full_access(); 46 61 let user_id: uuid::Uuid = 47 62 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 48 63 .fetch_optional(&state.db) ··· 73 88 .into_response(); 74 89 } 75 90 }; 76 - let preferences: Vec<Value> = prefs 91 + let mut personal_details_pref: Option<Value> = None; 92 + let mut preferences: Vec<Value> = prefs 77 93 .into_iter() 78 94 .filter(|row| { 79 95 row.name == APP_BSKY_NAMESPACE 80 96 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 81 97 }) 82 98 .filter_map(|row| { 83 - if row.name == "app.bsky.actor.defs#declaredAgePref" { 99 + if row.name == DECLARED_AGE_PREF { 84 100 return None; 85 101 } 102 + if row.name == PERSONAL_DETAILS_PREF { 103 + if !has_full_access { 104 + return None; 105 + } 106 + personal_details_pref = serde_json::from_value(row.value_json.clone()).ok(); 107 + } 86 108 serde_json::from_value(row.value_json).ok() 87 109 }) 88 110 .collect(); 111 + if let Some(ref pref) = personal_details_pref { 112 + if let Some(birth_date) = pref.get("birthDate").and_then(|v| v.as_str()) { 113 + if let Some(age) = get_age_from_datestring(birth_date) { 114 + let declared_age_pref = json!({ 115 + "$type": DECLARED_AGE_PREF, 116 + "isOverAge13": age >= 13, 117 + "isOverAge16": age >= 16, 118 + "isOverAge18": age >= 18, 119 + }); 120 + preferences.push(declared_age_pref); 121 + } 122 + } 123 + } 89 124 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 90 125 } 91 126 ··· 121 156 .into_response(); 122 157 } 123 158 }; 124 - let (user_id, is_migration): (uuid::Uuid, bool) = match sqlx::query!( 125 - "SELECT id, deactivated_at FROM users WHERE did = $1", 159 + let has_full_access = auth_user.permissions().has_full_access(); 160 + let user_id: uuid::Uuid = match sqlx::query_scalar!( 161 + "SELECT id FROM users WHERE did = $1", 126 162 auth_user.did 127 163 ) 128 164 .fetch_optional(&state.db) 129 165 .await 130 166 { 131 - Ok(Some(row)) => (row.id, row.deactivated_at.is_some()), 167 + Ok(Some(id)) => id, 132 168 _ => { 133 169 return ( 134 170 StatusCode::INTERNAL_SERVER_ERROR, ··· 144 180 ) 145 181 .into_response(); 146 182 } 183 + let mut forbidden_prefs: Vec<String> = Vec::new(); 147 184 for pref in &input.preferences { 148 185 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 149 186 if pref_str.len() > MAX_PREFERENCE_SIZE { ··· 158 195 None => { 159 196 return ( 160 197 StatusCode::BAD_REQUEST, 161 - Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})), 198 + Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})), 162 199 ) 163 200 .into_response(); 164 201 } ··· 166 203 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 167 204 return ( 168 205 StatusCode::BAD_REQUEST, 169 - Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})), 206 + Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})), 170 207 ) 171 208 .into_response(); 172 209 } 173 - if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration { 174 - return ( 175 - StatusCode::BAD_REQUEST, 176 - Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})), 177 - ) 178 - .into_response(); 210 + if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { 211 + forbidden_prefs.push(pref_type.to_string()); 179 212 } 213 + } 214 + if !forbidden_prefs.is_empty() { 215 + return ( 216 + StatusCode::BAD_REQUEST, 217 + Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})), 218 + ) 219 + .into_response(); 180 220 } 181 221 let mut tx = match state.db.begin().await { 182 222 Ok(tx) => tx, ··· 209 249 Some(t) => t, 210 250 None => continue, 211 251 }; 252 + if pref_type == DECLARED_AGE_PREF { 253 + continue; 254 + } 212 255 let insert_result = sqlx::query!( 213 256 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 214 257 user_id,
+7
src/api/identity/account.rs
··· 188 188 }; 189 189 match crate::api::validation::validate_short_handle(handle_to_validate) { 190 190 Ok(h) => h, 191 + Err(crate::api::validation::HandleValidationError::Reserved) => { 192 + return ( 193 + StatusCode::BAD_REQUEST, 194 + Json(json!({"error": "HandleNotAvailable", "message": "Reserved handle"})), 195 + ) 196 + .into_response(); 197 + } 191 198 Err(e) => { 192 199 return ( 193 200 StatusCode::BAD_REQUEST,
+35
src/api/proxy.rs
··· 10 10 use serde_json::json; 11 11 use tracing::{error, info, warn}; 12 12 13 + const PROTECTED_METHODS: &[&str] = &[ 14 + "com.atproto.admin.sendEmail", 15 + "com.atproto.identity.requestPlcOperationSignature", 16 + "com.atproto.identity.signPlcOperation", 17 + "com.atproto.identity.updateHandle", 18 + "com.atproto.server.activateAccount", 19 + "com.atproto.server.confirmEmail", 20 + "com.atproto.server.createAppPassword", 21 + "com.atproto.server.deactivateAccount", 22 + "com.atproto.server.getAccountInviteCodes", 23 + "com.atproto.server.getSession", 24 + "com.atproto.server.listAppPasswords", 25 + "com.atproto.server.requestAccountDelete", 26 + "com.atproto.server.requestEmailConfirmation", 27 + "com.atproto.server.requestEmailUpdate", 28 + "com.atproto.server.revokeAppPassword", 29 + "com.atproto.server.updateEmail", 30 + ]; 31 + 32 + fn is_protected_method(method: &str) -> bool { 33 + PROTECTED_METHODS.contains(&method) 34 + } 35 + 13 36 pub async fn proxy_handler( 14 37 State(state): State<AppState>, 15 38 Path(method): Path<String>, ··· 18 41 RawQuery(query): RawQuery, 19 42 body: Bytes, 20 43 ) -> Response { 44 + if is_protected_method(&method) { 45 + warn!(method = %method, "Attempted to proxy protected method"); 46 + return ( 47 + StatusCode::BAD_REQUEST, 48 + Json(json!({ 49 + "error": "InvalidRequest", 50 + "message": format!("Cannot proxy protected method: {}", method) 51 + })), 52 + ) 53 + .into_response(); 54 + } 55 + 21 56 let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { 22 57 Some(h) => h.to_string(), 23 58 None => {
+78 -2
src/api/validation.rs
··· 6 6 7 7 pub const MIN_HANDLE_LENGTH: usize = 3; 8 8 pub const MAX_HANDLE_LENGTH: usize = 253; 9 + pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; 9 10 10 11 #[derive(Debug, PartialEq)] 11 12 pub enum HandleValidationError { ··· 17 18 EndsWithInvalidChar, 18 19 ContainsSpaces, 19 20 BannedWord, 21 + Reserved, 20 22 } 21 23 22 24 impl std::fmt::Display for HandleValidationError { ··· 31 33 Self::TooLong => write!( 32 34 f, 33 35 "Handle exceeds maximum length of {} characters", 34 - MAX_HANDLE_LENGTH 36 + MAX_SERVICE_HANDLE_LOCAL_PART 35 37 ), 36 38 Self::InvalidCharacters => write!( 37 39 f, ··· 43 45 Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 44 46 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 45 47 Self::BannedWord => write!(f, "Inappropriate language in handle"), 48 + Self::Reserved => write!(f, "Reserved handle"), 46 49 } 47 50 } 48 51 } 49 52 50 53 pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { 54 + validate_service_handle(handle, false) 55 + } 56 + 57 + pub fn validate_service_handle( 58 + handle: &str, 59 + allow_reserved: bool, 60 + ) -> Result<String, HandleValidationError> { 51 61 let handle = handle.trim(); 52 62 53 63 if handle.is_empty() { ··· 62 72 return Err(HandleValidationError::TooShort); 63 73 } 64 74 65 - if handle.len() > MAX_HANDLE_LENGTH { 75 + if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART { 66 76 return Err(HandleValidationError::TooLong); 67 77 } 68 78 ··· 86 96 87 97 if crate::moderation::has_explicit_slur(handle) { 88 98 return Err(HandleValidationError::BannedWord); 99 + } 100 + 101 + if !allow_reserved && crate::handle::reserved::is_reserved_subdomain(handle) { 102 + return Err(HandleValidationError::Reserved); 89 103 } 90 104 91 105 Ok(handle.to_lowercase()) ··· 221 235 #[test] 222 236 fn test_handle_trimming() { 223 237 assert_eq!(validate_short_handle(" alice "), Ok("alice".to_string())); 238 + } 239 + 240 + #[test] 241 + fn test_handle_max_length() { 242 + assert_eq!( 243 + validate_short_handle("exactly18charslol"), 244 + Ok("exactly18charslol".to_string()) 245 + ); 246 + assert_eq!( 247 + validate_short_handle("exactly18charslol1"), 248 + Ok("exactly18charslol1".to_string()) 249 + ); 250 + assert_eq!( 251 + validate_short_handle("exactly19characters"), 252 + Err(HandleValidationError::TooLong) 253 + ); 254 + assert_eq!( 255 + validate_short_handle("waytoolongusername123456789"), 256 + Err(HandleValidationError::TooLong) 257 + ); 258 + } 259 + 260 + #[test] 261 + fn test_reserved_subdomains() { 262 + assert_eq!( 263 + validate_short_handle("admin"), 264 + Err(HandleValidationError::Reserved) 265 + ); 266 + assert_eq!( 267 + validate_short_handle("api"), 268 + Err(HandleValidationError::Reserved) 269 + ); 270 + assert_eq!( 271 + validate_short_handle("bsky"), 272 + Err(HandleValidationError::Reserved) 273 + ); 274 + assert_eq!( 275 + validate_short_handle("barackobama"), 276 + Err(HandleValidationError::Reserved) 277 + ); 278 + assert_eq!( 279 + validate_short_handle("ADMIN"), 280 + Err(HandleValidationError::Reserved) 281 + ); 282 + assert_eq!(validate_short_handle("alice"), Ok("alice".to_string())); 283 + assert_eq!( 284 + validate_short_handle("notreserved"), 285 + Ok("notreserved".to_string()) 286 + ); 287 + } 288 + 289 + #[test] 290 + fn test_allow_reserved() { 291 + assert_eq!( 292 + validate_service_handle("admin", true), 293 + Ok("admin".to_string()) 294 + ); 295 + assert_eq!(validate_service_handle("api", true), Ok("api".to_string())); 296 + assert_eq!( 297 + validate_service_handle("admin", false), 298 + Err(HandleValidationError::Reserved) 299 + ); 224 300 } 225 301 226 302 #[test]
+2
src/handle/mod.rs
··· 1 + pub mod reserved; 2 + 1 3 use hickory_resolver::TokioAsyncResolver; 2 4 use hickory_resolver::config::{ResolverConfig, ResolverOpts}; 3 5 use reqwest::Client;
+1097
src/handle/reserved.rs
··· 1 + use std::collections::HashSet; 2 + use std::sync::LazyLock; 3 + 4 + const ATP_SPECIFIC: &[&str] = &[ 5 + "at", 6 + "atp", 7 + "plc", 8 + "pds", 9 + "did", 10 + "repo", 11 + "tid", 12 + "nsid", 13 + "xrpc", 14 + "lex", 15 + "lexicon", 16 + "bsky", 17 + "bluesky", 18 + "handle", 19 + ]; 20 + 21 + const COMMONLY_RESERVED: &[&str] = &[ 22 + "about", 23 + "abuse", 24 + "access", 25 + "account", 26 + "accounts", 27 + "acme", 28 + "activate", 29 + "activities", 30 + "activity", 31 + "ad", 32 + "add", 33 + "address", 34 + "adm", 35 + "admanager", 36 + "admin", 37 + "administration", 38 + "administrator", 39 + "administrators", 40 + "admins", 41 + "ads", 42 + "adsense", 43 + "adult", 44 + "advertising", 45 + "adwords", 46 + "affiliate", 47 + "affiliatepage", 48 + "affiliates", 49 + "afp", 50 + "ajax", 51 + "all", 52 + "alpha", 53 + "analysis", 54 + "analytics", 55 + "android", 56 + "anon", 57 + "anonymous", 58 + "answer", 59 + "answers", 60 + "ap", 61 + "api", 62 + "apis", 63 + "app", 64 + "appengine", 65 + "appnews", 66 + "apps", 67 + "archive", 68 + "archives", 69 + "article", 70 + "asdf", 71 + "asset", 72 + "assets", 73 + "auth", 74 + "authentication", 75 + "avatar", 76 + "backup", 77 + "bank", 78 + "banner", 79 + "banners", 80 + "base", 81 + "beginners", 82 + "beta", 83 + "billing", 84 + "bin", 85 + "binaries", 86 + "binary", 87 + "blackberry", 88 + "blog", 89 + "blogs", 90 + "blogsearch", 91 + "board", 92 + "book", 93 + "bookmark", 94 + "bookmarks", 95 + "books", 96 + "bot", 97 + "bots", 98 + "bug", 99 + "bugs", 100 + "business", 101 + "buy", 102 + "buzz", 103 + "cache", 104 + "calendar", 105 + "call", 106 + "campaign", 107 + "cancel", 108 + "captcha", 109 + "career", 110 + "careers", 111 + "cart", 112 + "catalog", 113 + "catalogs", 114 + "categories", 115 + "category", 116 + "cdn", 117 + "cgi", 118 + "cgi-bin", 119 + "changelog", 120 + "chart", 121 + "charts", 122 + "chat", 123 + "check", 124 + "checked", 125 + "checking", 126 + "checkout", 127 + "client", 128 + "cliente", 129 + "clients", 130 + "clients1", 131 + "cnarne", 132 + "code", 133 + "comercial", 134 + "comment", 135 + "comments", 136 + "communities", 137 + "community", 138 + "company", 139 + "compare", 140 + "compras", 141 + "config", 142 + "configuration", 143 + "confirm", 144 + "confirmation", 145 + "connect", 146 + "contact", 147 + "contacts", 148 + "contactus", 149 + "contact-us", 150 + "contact_us", 151 + "content", 152 + "contest", 153 + "contribute", 154 + "contributor", 155 + "contributors", 156 + "coppa", 157 + "copyright", 158 + "copyrights", 159 + "core", 160 + "corp", 161 + "countries", 162 + "country", 163 + "cpanel", 164 + "create", 165 + "css", 166 + "cssproxy", 167 + "customise", 168 + "customize", 169 + "dashboard", 170 + "data", 171 + "db", 172 + "default", 173 + "delete", 174 + "demo", 175 + "design", 176 + "designer", 177 + "desktop", 178 + "destroy", 179 + "dev", 180 + "devel", 181 + "developer", 182 + "developers", 183 + "devs", 184 + "diagram", 185 + "diary", 186 + "dict", 187 + "dictionary", 188 + "die", 189 + "dir", 190 + "directory", 191 + "direct_messages", 192 + "direct-messages", 193 + "dist", 194 + "diversity", 195 + "dl", 196 + "dmca", 197 + "doc", 198 + "docs", 199 + "documentation", 200 + "documentations", 201 + "documents", 202 + "domain", 203 + "domains", 204 + "donate", 205 + "download", 206 + "downloads", 207 + "e", 208 + "e-mail", 209 + "earth", 210 + "ecommerce", 211 + "edit", 212 + "edits", 213 + "editor", 214 + "edu", 215 + "education", 216 + "email", 217 + "embed", 218 + "embedded", 219 + "employment", 220 + "employments", 221 + "empty", 222 + "enable", 223 + "encrypted", 224 + "end", 225 + "engine", 226 + "enterprise", 227 + "enterprises", 228 + "entries", 229 + "entry", 230 + "error", 231 + "errorlog", 232 + "errors", 233 + "eval", 234 + "event", 235 + "example", 236 + "examplecommunity", 237 + "exampleopenid", 238 + "examplesyn", 239 + "examplesyndicated", 240 + "exampleusername", 241 + "exchange", 242 + "exit", 243 + "explore", 244 + "faq", 245 + "faqs", 246 + "favorite", 247 + "favorites", 248 + "favourite", 249 + "favourites", 250 + "feature", 251 + "features", 252 + "feed", 253 + "feedback", 254 + "feedburner", 255 + "feedproxy", 256 + "feeds", 257 + "file", 258 + "files", 259 + "finance", 260 + "folder", 261 + "folders", 262 + "first", 263 + "following", 264 + "forgot", 265 + "form", 266 + "forms", 267 + "forum", 268 + "forums", 269 + "founder", 270 + "free", 271 + "friend", 272 + "friends", 273 + "ftp", 274 + "fuck", 275 + "fun", 276 + "fusion", 277 + "gadget", 278 + "gadgets", 279 + "game", 280 + "games", 281 + "gears", 282 + "general", 283 + "geographic", 284 + "get", 285 + "gettingstarted", 286 + "gift", 287 + "gifts", 288 + "gist", 289 + "git", 290 + "github", 291 + "gmail", 292 + "go", 293 + "golang", 294 + "goto", 295 + "gov", 296 + "graph", 297 + "graphs", 298 + "group", 299 + "groups", 300 + "guest", 301 + "guests", 302 + "guide", 303 + "guides", 304 + "hack", 305 + "hacks", 306 + "head", 307 + "help", 308 + "home", 309 + "homepage", 310 + "host", 311 + "hosting", 312 + "hostmaster", 313 + "hostname", 314 + "howto", 315 + "how-to", 316 + "how_to", 317 + "html", 318 + "htrnl", 319 + "http", 320 + "httpd", 321 + "https", 322 + "i", 323 + "iamges", 324 + "icon", 325 + "icons", 326 + "id", 327 + "idea", 328 + "ideas", 329 + "im", 330 + "image", 331 + "images", 332 + "img", 333 + "imap", 334 + "inbox", 335 + "inboxes", 336 + "index", 337 + "indexes", 338 + "info", 339 + "information", 340 + "inquiry", 341 + "intranet", 342 + "investor", 343 + "investors", 344 + "invitation", 345 + "invitations", 346 + "invite", 347 + "invoice", 348 + "invoices", 349 + "imac", 350 + "ios", 351 + "ipad", 352 + "iphone", 353 + "irc", 354 + "irnages", 355 + "irng", 356 + "is", 357 + "issue", 358 + "issues", 359 + "it", 360 + "item", 361 + "items", 362 + "java", 363 + "javascript", 364 + "job", 365 + "jobs", 366 + "join", 367 + "js", 368 + "json", 369 + "jump", 370 + "kb", 371 + "knowledge-base", 372 + "knowledgebase", 373 + "lab", 374 + "labs", 375 + "language", 376 + "languages", 377 + "last", 378 + "ldap_status", 379 + "ldap-status", 380 + "ldapstatus", 381 + "legal", 382 + "license", 383 + "licenses", 384 + "link", 385 + "links", 386 + "linux", 387 + "list", 388 + "lists", 389 + "livejournal", 390 + "lj", 391 + "local", 392 + "locale", 393 + "location", 394 + "log", 395 + "log-in", 396 + "log-out", 397 + "login", 398 + "logout", 399 + "logs", 400 + "log_in", 401 + "log_out", 402 + "m", 403 + "mac", 404 + "macos", 405 + "macosx", 406 + "mac-os", 407 + "mac-os-x", 408 + "mac_os_x", 409 + "mail", 410 + "mailer", 411 + "mailing", 412 + "main", 413 + "maintenance", 414 + "manage", 415 + "manager", 416 + "manual", 417 + "map", 418 + "maps", 419 + "marketing", 420 + "master", 421 + "me", 422 + "media", 423 + "member", 424 + "members", 425 + "memories", 426 + "memory", 427 + "merchandise", 428 + "message", 429 + "messages", 430 + "messenger", 431 + "mg", 432 + "microblog", 433 + "microblogs", 434 + "mine", 435 + "mis", 436 + "misc", 437 + "mms", 438 + "mob", 439 + "mobile", 440 + "model", 441 + "models", 442 + "money", 443 + "movie", 444 + "movies", 445 + "mp3", 446 + "mp4", 447 + "msg", 448 + "msn", 449 + "music", 450 + "mx", 451 + "my", 452 + "mymme", 453 + "mysql", 454 + "name", 455 + "named", 456 + "nan", 457 + "navi", 458 + "navigation", 459 + "net", 460 + "network", 461 + "networks", 462 + "new", 463 + "news", 464 + "newsletter", 465 + "nick", 466 + "nickname", 467 + "nil", 468 + "none", 469 + "notes", 470 + "noticias", 471 + "notification", 472 + "notifications", 473 + "notify", 474 + "ns", 475 + "ns1", 476 + "ns2", 477 + "ns3", 478 + "ns4", 479 + "ns5", 480 + "null", 481 + "oauth", 482 + "oauth-clients", 483 + "oauth_clients", 484 + "ocsp", 485 + "offer", 486 + "offers", 487 + "official", 488 + "old", 489 + "online", 490 + "openid", 491 + "operator", 492 + "option", 493 + "options", 494 + "order", 495 + "orders", 496 + "org", 497 + "organization", 498 + "organizations", 499 + "other", 500 + "overview", 501 + "owner", 502 + "owners", 503 + "p0rn", 504 + "pack", 505 + "page", 506 + "pager", 507 + "pages", 508 + "paid", 509 + "panel", 510 + "partner", 511 + "partnerpage", 512 + "partners", 513 + "password", 514 + "patch", 515 + "pay", 516 + "payment", 517 + "people", 518 + "perl", 519 + "person", 520 + "phone", 521 + "photo", 522 + "photoalbum", 523 + "photos", 524 + "php", 525 + "phpmyadmin", 526 + "phppgadmin", 527 + "phpredisadmin", 528 + "pic", 529 + "pics", 530 + "picture", 531 + "pictures", 532 + "ping", 533 + "pixel", 534 + "places", 535 + "plan", 536 + "plans", 537 + "plugin", 538 + "plugins", 539 + "podcasts", 540 + "policies", 541 + "policy", 542 + "pop", 543 + "pop3", 544 + "popular", 545 + "porn", 546 + "portal", 547 + "portals", 548 + "post", 549 + "postfix", 550 + "postmaster", 551 + "posts", 552 + "pr", 553 + "pr0n", 554 + "premium", 555 + "press", 556 + "price", 557 + "pricing", 558 + "principles", 559 + "print", 560 + "privacy", 561 + "privacy-policy", 562 + "privacypolicy", 563 + "privacy_policy", 564 + "private", 565 + "prod", 566 + "product", 567 + "production", 568 + "products", 569 + "profile", 570 + "profiles", 571 + "project", 572 + "projects", 573 + "promo", 574 + "promotions", 575 + "proxies", 576 + "proxy", 577 + "pub", 578 + "public", 579 + "purchase", 580 + "purpose", 581 + "put", 582 + "python", 583 + "queries", 584 + "query", 585 + "radio", 586 + "random", 587 + "ranking", 588 + "read", 589 + "reader", 590 + "readme", 591 + "recent", 592 + "recruit", 593 + "recruitment", 594 + "redirect", 595 + "register", 596 + "registration", 597 + "release", 598 + "remove", 599 + "replies", 600 + "report", 601 + "reports", 602 + "repositories", 603 + "repository", 604 + "req", 605 + "request", 606 + "requests", 607 + "research", 608 + "reset", 609 + "resolve", 610 + "resolver", 611 + "review", 612 + "rnail", 613 + "rnicrosoft", 614 + "roc", 615 + "root", 616 + "rss", 617 + "ruby", 618 + "rule", 619 + "sag", 620 + "sale", 621 + "sales", 622 + "sample", 623 + "samples", 624 + "sandbox", 625 + "save", 626 + "scholar", 627 + "school", 628 + "schools", 629 + "script", 630 + "scripts", 631 + "search", 632 + "secure", 633 + "security", 634 + "self", 635 + "seminars", 636 + "send", 637 + "server", 638 + "server-info", 639 + "server_info", 640 + "server-status", 641 + "server_status", 642 + "servers", 643 + "service", 644 + "services", 645 + "session", 646 + "sessions", 647 + "setting", 648 + "settings", 649 + "setup", 650 + "share", 651 + "shop", 652 + "shopping", 653 + "shortcut", 654 + "shortcuts", 655 + "show", 656 + "sign-in", 657 + "sign-up", 658 + "signin", 659 + "signout", 660 + "signup", 661 + "sign_in", 662 + "sign_up", 663 + "site", 664 + "sitemap", 665 + "sitemaps", 666 + "sitenews", 667 + "sites", 668 + "sketchup", 669 + "sky", 670 + "slash", 671 + "slashinvoice", 672 + "slut", 673 + "smartphone", 674 + "sms", 675 + "smtp", 676 + "soap", 677 + "software", 678 + "sorry", 679 + "source", 680 + "spec", 681 + "special", 682 + "spreadsheet", 683 + "spreadsheets", 684 + "sql", 685 + "src", 686 + "srntp", 687 + "ssh", 688 + "ssl", 689 + "ssladmin", 690 + "ssladministrator", 691 + "sslwebmaster", 692 + "ssytem", 693 + "staff", 694 + "stage", 695 + "staging", 696 + "start", 697 + "stat", 698 + "state", 699 + "static", 700 + "statistics", 701 + "stats", 702 + "status", 703 + "store", 704 + "stores", 705 + "stories", 706 + "style", 707 + "styleguide", 708 + "styles", 709 + "stylesheet", 710 + "stylesheets", 711 + "subdomain", 712 + "subscribe", 713 + "subscription", 714 + "subscriptions", 715 + "suggest", 716 + "suggestqueries", 717 + "support", 718 + "survey", 719 + "surveys", 720 + "surveytool", 721 + "svn", 722 + "swf", 723 + "syn", 724 + "sync", 725 + "syndicated", 726 + "sys", 727 + "sysadmin", 728 + "sysadministrator", 729 + "sysadmins", 730 + "system", 731 + "tablet", 732 + "tablets", 733 + "tag", 734 + "tags", 735 + "talk", 736 + "talkgadget", 737 + "task", 738 + "tasks", 739 + "team", 740 + "teams", 741 + "tech", 742 + "telnet", 743 + "term", 744 + "terms", 745 + "terms-of-service", 746 + "termsofservice", 747 + "terms_of_service", 748 + "test", 749 + "testing", 750 + "tests", 751 + "text", 752 + "theme", 753 + "themes", 754 + "thread", 755 + "threads", 756 + "ticket", 757 + "tickets", 758 + "tmp", 759 + "todo", 760 + "to-do", 761 + "to_do", 762 + "toml", 763 + "tool", 764 + "toolbar", 765 + "toolbars", 766 + "tools", 767 + "top", 768 + "topic", 769 + "topics", 770 + "tos", 771 + "tour", 772 + "trac", 773 + "translate", 774 + "trace", 775 + "translation", 776 + "translations", 777 + "translator", 778 + "trends", 779 + "tutorial", 780 + "tux", 781 + "tv", 782 + "twitter", 783 + "txt", 784 + "ul", 785 + "undef", 786 + "unfollow", 787 + "unsubscribe", 788 + "update", 789 + "updates", 790 + "upgrade", 791 + "upgrades", 792 + "upi", 793 + "upload", 794 + "uploads", 795 + "url", 796 + "usage", 797 + "user", 798 + "username", 799 + "usernames", 800 + "users", 801 + "uuid", 802 + "validation", 803 + "validations", 804 + "ver", 805 + "version", 806 + "video", 807 + "videos", 808 + "video-stats", 809 + "visitor", 810 + "visitors", 811 + "voice", 812 + "volunteer", 813 + "volunteers", 814 + "w", 815 + "watch", 816 + "wave", 817 + "weather", 818 + "web", 819 + "webdisk", 820 + "webhook", 821 + "webhooks", 822 + "webmail", 823 + "webmaster", 824 + "webmasters", 825 + "webrnail", 826 + "website", 827 + "websites", 828 + "welcome", 829 + "whm", 830 + "whois", 831 + "widget", 832 + "widgets", 833 + "wifi", 834 + "wiki", 835 + "wikis", 836 + "win", 837 + "windows", 838 + "word", 839 + "work", 840 + "works", 841 + "workshop", 842 + "wpad", 843 + "ww", 844 + "wws", 845 + "www", 846 + "wwws", 847 + "wwww", 848 + "xfn", 849 + "xhtml", 850 + "xhtrnl", 851 + "xml", 852 + "xmpp", 853 + "xpg", 854 + "xxx", 855 + "yaml", 856 + "year", 857 + "yml", 858 + "you", 859 + "yourdomain", 860 + "yourname", 861 + "yoursite", 862 + "yourusername", 863 + ]; 864 + 865 + const FAMOUS_ACCOUNTS: &[&str] = &[ 866 + "10ronaldinho", 867 + "3gerardpique", 868 + "aclu", 869 + "adele", 870 + "akshaykumar", 871 + "aliaa08", 872 + "aliciakeys", 873 + "amitshah", 874 + "andresiniesta8", 875 + "anushkasharma", 876 + "arianagrande", 877 + "arrahman", 878 + "arvindkejriwal", 879 + "avrillavigne", 880 + "barackobama", 881 + "bbcbreaking", 882 + "bbcworld", 883 + "beingsalmankhan", 884 + "billgates", 885 + "britneyspears", 886 + "brunomars", 887 + "bts_bighit", 888 + "bts_twt", 889 + "championsleague", 890 + "chrisbrown", 891 + "cnnbrk", 892 + "coldplay", 893 + "conanobrien", 894 + "cristiano", 895 + "danieltosh", 896 + "davidguetta", 897 + "ddlovato", 898 + "deepikapadukone", 899 + "drake", 900 + "elisapie", 901 + "ellendegeneres", 902 + "elonmusk", 903 + "eminem", 904 + "emmawatson", 905 + "fcbarcelona", 906 + "foxnews", 907 + "harry_styles", 908 + "hillaryclinton", 909 + "iamsrk", 910 + "ihrithik", 911 + "imvkohli", 912 + "instagram", 913 + "jimmyfallon", 914 + "jlo", 915 + "joebiden", 916 + "jtimberlake", 917 + "justinbieber", 918 + "kaka", 919 + "kanyewest", 920 + "katyperry", 921 + "kendalljenner", 922 + "kevinhart4real", 923 + "khloekardashian", 924 + "kimkardashian", 925 + "kingjames", 926 + "kourtneykardash", 927 + "kyliejenner", 928 + "ladygaga", 929 + "liampayne", 930 + "liltunechi", 931 + "manutd", 932 + "mariahcarey", 933 + "mileycyrus", 934 + "mohamadalarefe", 935 + "narendramodi", 936 + "nasa", 937 + "nba", 938 + "neymarjr", 939 + "nfl", 940 + "niallofficial", 941 + "nickiminaj", 942 + "npr", 943 + "nytimes", 944 + "onedirection", 945 + "oprah", 946 + "pink", 947 + "pitbull", 948 + "playstation", 949 + "pmoindia", 950 + "premierleague", 951 + "priyankachopra", 952 + "realdonaldtrump", 953 + "ricky_martin", 954 + "rihanna", 955 + "sachin_rt", 956 + "selenagomez", 957 + "shakira", 958 + "shawnmendes", 959 + "sportscenter", 960 + "srbachchan", 961 + "subhisharma100", 962 + "taylorswift13", 963 + "theeconomist", 964 + "twitter", 965 + "virendersehwag", 966 + "whitehouse45", 967 + "wizkhalifa", 968 + "youtube", 969 + "zaynmalik", 970 + "beyonce", 971 + "billieeilish", 972 + "leomessi", 973 + "natgeo", 974 + "nike", 975 + "snoopdogg", 976 + "taylorswift", 977 + "therock", 978 + "10downingstreet", 979 + "aoc", 980 + "carterjwm", 981 + "dril", 982 + "gretathunberg", 983 + "kamalaharris", 984 + "kremlinrussia_e", 985 + "potus", 986 + "rondesantisfl", 987 + "ukraine", 988 + "washingtonpost", 989 + "yousuck2020", 990 + "zelenskyyua", 991 + "akiko_lawson", 992 + "ariyoshihiroiki", 993 + "asahi", 994 + "dozle_official", 995 + "famima_now", 996 + "ff_xiv_jp", 997 + "fujitv", 998 + "gigazine", 999 + "hajimesyacho", 1000 + "hikakin", 1001 + "jocx", 1002 + "jotx", 1003 + "kiyo_saiore", 1004 + "mainichi", 1005 + "matsu_bouzu", 1006 + "naomiosaka", 1007 + "nhk", 1008 + "nikkei", 1009 + "nintendo", 1010 + "ntv", 1011 + "oowareware1945", 1012 + "pamyurin", 1013 + "poke_times", 1014 + "rolaworld", 1015 + "seikintv", 1016 + "starbucksjapan", 1017 + "tbs", 1018 + "tbs_pr", 1019 + "tvasahi", 1020 + "tvtokyo", 1021 + "yokoono", 1022 + "yomiuri_online", 1023 + "brasildefato", 1024 + "claudialeitte", 1025 + "correio", 1026 + "em_com", 1027 + "estadao", 1028 + "folha", 1029 + "gazetadopovo", 1030 + "ivetesangalo", 1031 + "jairbolsonaro", 1032 + "jornaldobrasil", 1033 + "jornaloglobo", 1034 + "lucianohuck", 1035 + "lulaoficial", 1036 + "marcosmion", 1037 + "paulocoelho", 1038 + "portalr7", 1039 + "rede_globo", 1040 + "zerohora", 1041 + ]; 1042 + 1043 + pub static RESERVED_SUBDOMAINS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { 1044 + let mut set = HashSet::with_capacity( 1045 + ATP_SPECIFIC.len() + COMMONLY_RESERVED.len() + FAMOUS_ACCOUNTS.len(), 1046 + ); 1047 + for s in ATP_SPECIFIC { 1048 + set.insert(*s); 1049 + } 1050 + for s in COMMONLY_RESERVED { 1051 + set.insert(*s); 1052 + } 1053 + for s in FAMOUS_ACCOUNTS { 1054 + set.insert(*s); 1055 + } 1056 + set 1057 + }); 1058 + 1059 + pub fn is_reserved_subdomain(subdomain: &str) -> bool { 1060 + RESERVED_SUBDOMAINS.contains(subdomain.to_lowercase().as_str()) 1061 + } 1062 + 1063 + #[cfg(test)] 1064 + mod tests { 1065 + use super::*; 1066 + 1067 + #[test] 1068 + fn test_atp_specific_reserved() { 1069 + assert!(is_reserved_subdomain("admin")); 1070 + assert!(is_reserved_subdomain("api")); 1071 + assert!(is_reserved_subdomain("bsky")); 1072 + assert!(is_reserved_subdomain("plc")); 1073 + assert!(is_reserved_subdomain("xrpc")); 1074 + } 1075 + 1076 + #[test] 1077 + fn test_famous_accounts_reserved() { 1078 + assert!(is_reserved_subdomain("barackobama")); 1079 + assert!(is_reserved_subdomain("elonmusk")); 1080 + assert!(is_reserved_subdomain("taylorswift")); 1081 + assert!(is_reserved_subdomain("nintendo")); 1082 + } 1083 + 1084 + #[test] 1085 + fn test_case_insensitive() { 1086 + assert!(is_reserved_subdomain("ADMIN")); 1087 + assert!(is_reserved_subdomain("Admin")); 1088 + assert!(is_reserved_subdomain("BARACKOBAMA")); 1089 + } 1090 + 1091 + #[test] 1092 + fn test_not_reserved() { 1093 + assert!(!is_reserved_subdomain("alice")); 1094 + assert!(!is_reserved_subdomain("bob123")); 1095 + assert!(!is_reserved_subdomain("randomuser")); 1096 + } 1097 + }
+1
src/oauth/endpoints/metadata.rs
··· 81 81 "atproto".to_string(), 82 82 "transition:generic".to_string(), 83 83 "transition:chat.bsky".to_string(), 84 + "transition:email".to_string(), 84 85 "repo:*".to_string(), 85 86 "repo:*?action=create".to_string(), 86 87 "repo:*?action=read".to_string(),
+39 -62
src/sync/deprecated.rs
··· 1 + use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown}; 1 2 use crate::state::AppState; 2 3 use crate::sync::car::encode_car_header; 4 + use crate::sync::util::assert_repo_availability; 3 5 use axum::{ 4 6 Json, 5 7 extract::{Query, State}, 6 - http::StatusCode, 8 + http::{HeaderMap, StatusCode}, 7 9 response::{IntoResponse, Response}, 8 10 }; 9 11 use cid::Cid; ··· 13 15 use serde_json::json; 14 16 use std::io::Write; 15 17 use std::str::FromStr; 16 - use tracing::error; 17 18 18 19 const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; 19 20 21 + async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool { 22 + let token = match extract_bearer_token_from_header( 23 + headers.get("Authorization").and_then(|h| h.to_str().ok()), 24 + ) { 25 + Some(t) => t, 26 + None => return false, 27 + }; 28 + match validate_bearer_token_allow_takendown(&state.db, &token).await { 29 + Ok(auth_user) => auth_user.is_admin || auth_user.did == did, 30 + Err(_) => false, 31 + } 32 + } 33 + 20 34 #[derive(Deserialize)] 21 35 pub struct GetHeadParams { 22 36 pub did: String, ··· 29 43 30 44 pub async fn get_head( 31 45 State(state): State<AppState>, 46 + headers: HeaderMap, 32 47 Query(params): Query<GetHeadParams>, 33 48 ) -> Response { 34 49 let did = params.did.trim(); ··· 39 54 ) 40 55 .into_response(); 41 56 } 42 - let result = sqlx::query!( 43 - r#" 44 - SELECT r.repo_root_cid 45 - FROM repos r 46 - JOIN users u ON r.user_id = u.id 47 - WHERE u.did = $1 48 - "#, 49 - did 50 - ) 51 - .fetch_optional(&state.db) 52 - .await; 53 - match result { 54 - Ok(Some(row)) => ( 55 - StatusCode::OK, 56 - Json(GetHeadOutput { 57 - root: row.repo_root_cid, 58 - }), 59 - ) 60 - .into_response(), 61 - Ok(None) => ( 57 + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 58 + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { 59 + Ok(a) => a, 60 + Err(e) => return e.into_response(), 61 + }; 62 + match account.repo_root_cid { 63 + Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(), 64 + None => ( 62 65 StatusCode::BAD_REQUEST, 63 - Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})), 66 + Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})), 64 67 ) 65 68 .into_response(), 66 - Err(e) => { 67 - error!("DB error in get_head: {:?}", e); 68 - ( 69 - StatusCode::INTERNAL_SERVER_ERROR, 70 - Json(json!({"error": "InternalError"})), 71 - ) 72 - .into_response() 73 - } 74 69 } 75 70 } 76 71 ··· 81 76 82 77 pub async fn get_checkout( 83 78 State(state): State<AppState>, 79 + headers: HeaderMap, 84 80 Query(params): Query<GetCheckoutParams>, 85 81 ) -> Response { 86 82 let did = params.did.trim(); ··· 91 87 ) 92 88 .into_response(); 93 89 } 94 - let repo_row = sqlx::query!( 95 - r#" 96 - SELECT r.repo_root_cid 97 - FROM repos r 98 - JOIN users u ON u.id = r.user_id 99 - WHERE u.did = $1 100 - "#, 101 - did 102 - ) 103 - .fetch_optional(&state.db) 104 - .await 105 - .unwrap_or(None); 106 - let head_str = match repo_row { 107 - Some(r) => r.repo_root_cid, 90 + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 91 + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { 92 + Ok(a) => a, 93 + Err(e) => return e.into_response(), 94 + }; 95 + let head_str = match account.repo_root_cid { 96 + Some(r) => r, 108 97 None => { 109 - let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 110 - .fetch_optional(&state.db) 111 - .await 112 - .unwrap_or(None); 113 - if user_exists.is_none() { 114 - return ( 115 - StatusCode::NOT_FOUND, 116 - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), 117 - ) 118 - .into_response(); 119 - } else { 120 - return ( 121 - StatusCode::NOT_FOUND, 122 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 123 - ) 124 - .into_response(); 125 - } 98 + return ( 99 + StatusCode::BAD_REQUEST, 100 + Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 101 + ) 102 + .into_response(); 126 103 } 127 104 }; 128 105 let head_cid = match Cid::from_str(&head_str) {
+3 -3
tests/account_lifecycle.rs
··· 154 154 let client = client(); 155 155 let base = base_url().await; 156 156 157 - let handle = format!("diddoctest-{}", uuid::Uuid::new_v4()); 157 + let handle = format!("dd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 158 158 let payload = json!({ 159 159 "handle": handle, 160 160 "email": format!("{}@example.com", handle), ··· 185 185 let client = client(); 186 186 let base = base_url().await; 187 187 188 - let handle = format!("tokentest-{}", uuid::Uuid::new_v4()); 188 + let handle = format!("tt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 189 189 let payload = json!({ 190 190 "handle": handle, 191 191 "email": format!("{}@example.com", handle), ··· 243 243 let client = client(); 244 244 let base = base_url().await; 245 245 246 - let handle = format!("pwdlentest-{}", uuid::Uuid::new_v4()); 246 + let handle = format!("pl{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 247 247 let payload = json!({ 248 248 "handle": handle, 249 249 "email": format!("{}@example.com", handle),
+105 -4
tests/actor.rs
··· 140 140 } 141 141 142 142 #[tokio::test] 143 - async fn test_put_preferences_read_only_rejected() { 143 + async fn test_put_preferences_read_only_silently_filtered() { 144 144 let client = client(); 145 145 let base = base_url().await; 146 146 let (token, _did) = create_account_and_login(&client).await; ··· 149 149 { 150 150 "$type": "app.bsky.actor.defs#declaredAgePref", 151 151 "isOverAge18": true 152 + }, 153 + { 154 + "$type": "app.bsky.actor.defs#adultContentPref", 155 + "enabled": true 152 156 } 153 157 ] 154 158 }); ··· 159 163 .send() 160 164 .await 161 165 .unwrap(); 162 - assert_eq!(resp.status(), 400); 163 - let body: Value = resp.json().await.unwrap(); 164 - assert_eq!(body["error"], "InvalidRequest"); 166 + assert_eq!(resp.status(), 200); 167 + let get_resp = client 168 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 169 + .header("Authorization", format!("Bearer {}", token)) 170 + .send() 171 + .await 172 + .unwrap(); 173 + assert_eq!(get_resp.status(), 200); 174 + let body: Value = get_resp.json().await.unwrap(); 175 + let prefs_arr = body["preferences"].as_array().unwrap(); 176 + assert_eq!(prefs_arr.len(), 1); 177 + assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#adultContentPref"); 165 178 } 166 179 167 180 #[tokio::test] ··· 328 341 let body: Value = resp.json().await.unwrap(); 329 342 assert!(body["preferences"].as_array().unwrap().is_empty()); 330 343 } 344 + 345 + #[tokio::test] 346 + async fn test_declared_age_pref_computed_from_birth_date() { 347 + let client = client(); 348 + let base = base_url().await; 349 + let (token, _did) = create_account_and_login(&client).await; 350 + let prefs = json!({ 351 + "preferences": [ 352 + { 353 + "$type": "app.bsky.actor.defs#personalDetailsPref", 354 + "birthDate": "1990-01-15" 355 + } 356 + ] 357 + }); 358 + let resp = client 359 + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) 360 + .header("Authorization", format!("Bearer {}", token)) 361 + .json(&prefs) 362 + .send() 363 + .await 364 + .unwrap(); 365 + assert_eq!(resp.status(), 200); 366 + let get_resp = client 367 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 368 + .header("Authorization", format!("Bearer {}", token)) 369 + .send() 370 + .await 371 + .unwrap(); 372 + assert_eq!(get_resp.status(), 200); 373 + let body: Value = get_resp.json().await.unwrap(); 374 + let prefs_arr = body["preferences"].as_array().unwrap(); 375 + assert_eq!(prefs_arr.len(), 2); 376 + let personal_details = prefs_arr 377 + .iter() 378 + .find(|p| p["$type"] == "app.bsky.actor.defs#personalDetailsPref"); 379 + assert!(personal_details.is_some()); 380 + assert_eq!(personal_details.unwrap()["birthDate"], "1990-01-15"); 381 + let declared_age = prefs_arr 382 + .iter() 383 + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); 384 + assert!(declared_age.is_some()); 385 + let declared_age = declared_age.unwrap(); 386 + assert_eq!(declared_age["isOverAge13"], true); 387 + assert_eq!(declared_age["isOverAge16"], true); 388 + assert_eq!(declared_age["isOverAge18"], true); 389 + } 390 + 391 + #[tokio::test] 392 + async fn test_declared_age_pref_computed_under_18() { 393 + let client = client(); 394 + let base = base_url().await; 395 + let (token, _did) = create_account_and_login(&client).await; 396 + let current_year = chrono::Utc::now().format("%Y").to_string().parse::<i32>().unwrap(); 397 + let birth_year = current_year - 15; 398 + let prefs = json!({ 399 + "preferences": [ 400 + { 401 + "$type": "app.bsky.actor.defs#personalDetailsPref", 402 + "birthDate": format!("{}-06-15", birth_year) 403 + } 404 + ] 405 + }); 406 + let resp = client 407 + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) 408 + .header("Authorization", format!("Bearer {}", token)) 409 + .json(&prefs) 410 + .send() 411 + .await 412 + .unwrap(); 413 + assert_eq!(resp.status(), 200); 414 + let get_resp = client 415 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 416 + .header("Authorization", format!("Bearer {}", token)) 417 + .send() 418 + .await 419 + .unwrap(); 420 + assert_eq!(get_resp.status(), 200); 421 + let body: Value = get_resp.json().await.unwrap(); 422 + let prefs_arr = body["preferences"].as_array().unwrap(); 423 + let declared_age = prefs_arr 424 + .iter() 425 + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); 426 + assert!(declared_age.is_some()); 427 + let declared_age = declared_age.unwrap(); 428 + assert_eq!(declared_age["isOverAge13"], true); 429 + assert_eq!(declared_age["isOverAge16"], false); 430 + assert_eq!(declared_age["isOverAge18"], false); 431 + }
+6 -6
tests/banned_words.rs
··· 31 31 #[tokio::test] 32 32 async fn test_handle_with_slur_rejected() { 33 33 let client = client(); 34 - let timestamp = chrono::Utc::now().timestamp_millis(); 34 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 35 35 let slur = decode("bmlnZ2Vy"); 36 - let offensive_handle = format!("{}{}", slur, timestamp); 36 + let offensive_handle = format!("{}{}", slur, suffix); 37 37 38 38 let create_payload = json!({ 39 39 "handle": offensive_handle, 40 - "email": format!("test{}@example.com", timestamp), 40 + "email": format!("test{}@example.com", suffix), 41 41 "password": "TestPassword123!" 42 42 }); 43 43 ··· 65 65 #[tokio::test] 66 66 async fn test_handle_with_normalized_slur_rejected() { 67 67 let client = client(); 68 - let timestamp = chrono::Utc::now().timestamp_millis(); 68 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..6]; 69 69 let slur = decode("bi1pLWctZy1lLXI="); 70 - let offensive_handle = format!("{}{}", slur, timestamp); 70 + let offensive_handle = format!("{}{}", slur, suffix); 71 71 72 72 let create_payload = json!({ 73 73 "handle": offensive_handle, 74 - "email": format!("test{}@example.com", timestamp), 74 + "email": format!("test{}@example.com", suffix), 75 75 "password": "TestPassword123!" 76 76 }); 77 77
+1 -1
tests/common/mod.rs
··· 440 440 if attempt > 0 { 441 441 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 442 442 } 443 - let handle = format!("user-{}", uuid::Uuid::new_v4()); 443 + let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 444 444 let payload = json!({ 445 445 "handle": handle, 446 446 "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!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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
+12 -12
tests/email_update.rs
··· 57 57 async fn test_request_email_update_returns_token_required() { 58 58 let client = common::client(); 59 59 let base_url = common::base_url().await; 60 - let handle = format!("emailreq-{}", uuid::Uuid::new_v4()); 60 + let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 61 61 let email = format!("{}@example.com", handle); 62 62 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 63 63 ··· 80 80 let client = common::client(); 81 81 let base_url = common::base_url().await; 82 82 let pool = common::get_test_db_pool().await; 83 - let handle = format!("emailup-{}", uuid::Uuid::new_v4()); 83 + let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 84 84 let email = format!("{}@example.com", handle); 85 85 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 86 86 let new_email = format!("new_{}@example.com", handle); ··· 123 123 async fn test_update_email_requires_token_when_verified() { 124 124 let client = common::client(); 125 125 let base_url = common::base_url().await; 126 - let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); 126 + let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 127 127 let email = format!("{}@example.com", handle); 128 128 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 129 129 let new_email = format!("direct_{}@example.com", handle); ··· 144 144 async fn test_update_email_same_email_noop() { 145 145 let client = common::client(); 146 146 let base_url = common::base_url().await; 147 - let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); 147 + let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 148 148 let email = format!("{}@example.com", handle); 149 149 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 150 150 ··· 166 166 async fn test_update_email_invalid_token() { 167 167 let client = common::client(); 168 168 let base_url = common::base_url().await; 169 - let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); 169 + let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 170 170 let email = format!("{}@example.com", handle); 171 171 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 172 172 let new_email = format!("badtok_{}@example.com", handle); ··· 217 217 async fn test_update_email_invalid_format() { 218 218 let client = common::client(); 219 219 let base_url = common::base_url().await; 220 - let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); 220 + let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 221 221 let email = format!("{}@example.com", handle); 222 222 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 223 223 ··· 236 236 let client = common::client(); 237 237 let base_url = common::base_url().await; 238 238 let pool = common::get_test_db_pool().await; 239 - let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4()); 239 + let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 240 240 let email = format!("{}@example.com", handle); 241 241 242 242 let res = client ··· 298 298 let client = common::client(); 299 299 let base_url = common::base_url().await; 300 300 let pool = common::get_test_db_pool().await; 301 - let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4()); 301 + let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 302 302 let email = format!("{}@example.com", handle); 303 303 304 304 let res = client ··· 352 352 async fn test_confirm_email_invalid_token() { 353 353 let client = common::client(); 354 354 let base_url = common::base_url().await; 355 - let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4()); 355 + let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 356 356 let email = format!("{}@example.com", handle); 357 357 358 358 let res = client ··· 392 392 let client = common::client(); 393 393 let base_url = common::base_url().await; 394 394 let pool = common::get_test_db_pool().await; 395 - let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4()); 395 + let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 396 396 let email = format!("{}@example.com", handle); 397 397 398 398 let res = client ··· 457 457 let base_url = common::base_url().await; 458 458 let pool = common::get_test_db_pool().await; 459 459 460 - let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); 460 + let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 461 461 let email1 = format!("{}@example.com", handle1); 462 462 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 463 463 464 - let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); 464 + let handle2 = format!("d2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 465 465 let email2 = format!("{}@example.com", handle2); 466 466 let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await; 467 467
+4 -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!("rt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("dp{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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://");
+3 -3
tests/jwt_security.rs
··· 669 669 async fn test_refresh_token_replay_protection() { 670 670 let url = base_url().await; 671 671 let http_client = client(); 672 - let ts = Utc::now().timestamp_millis(); 673 - let handle = format!("rt-replay-jwt-{}", ts); 674 - let email = format!("rt-replay-jwt-{}@example.com", ts); 672 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 673 + let handle = format!("rr{}", suffix); 674 + let email = format!("rr{}@example.com", suffix); 675 675 676 676 let create_res = http_client 677 677 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+22 -22
tests/oauth.rs
··· 191 191 async fn test_full_oauth_flow() { 192 192 let url = base_url().await; 193 193 let http_client = client(); 194 - let ts = Utc::now().timestamp_millis(); 195 - let handle = format!("oauth-test-{}", ts); 196 - let email = format!("oauth-test-{}@example.com", ts); 194 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 195 + let handle = format!("ot{}", suffix); 196 + let email = format!("ot{}@example.com", suffix); 197 197 let password = "Oauthtest123!"; 198 198 let create_res = http_client 199 199 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 209 209 let mock_client = setup_mock_client_metadata(redirect_uri).await; 210 210 let client_id = mock_client.uri(); 211 211 let (code_verifier, code_challenge) = generate_pkce(); 212 - let state = format!("state-{}", ts); 212 + let state = format!("state-{}", suffix); 213 213 let par_res = http_client 214 214 .post(format!("{}/oauth/par", url)) 215 215 .form(&[ ··· 349 349 async fn test_oauth_error_cases() { 350 350 let url = base_url().await; 351 351 let http_client = client(); 352 - let ts = Utc::now().timestamp_millis(); 353 - let handle = format!("wrong-creds-{}", ts); 354 - let email = format!("wrong-creds-{}@example.com", ts); 352 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 353 + let handle = format!("wc{}", suffix); 354 + let email = format!("wc{}@example.com", suffix); 355 355 http_client 356 356 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 357 357 .json(&json!({ "handle": handle, "email": email, "password": "Correct123!" })) ··· 435 435 async fn test_oauth_2fa_flow() { 436 436 let url = base_url().await; 437 437 let http_client = client(); 438 - let ts = Utc::now().timestamp_millis(); 439 - let handle = format!("2fa-test-{}", ts); 440 - let email = format!("2fa-test-{}@example.com", ts); 438 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 439 + let handle = format!("ft{}", suffix); 440 + let email = format!("ft{}@example.com", suffix); 441 441 let password = "Twofa123test!"; 442 442 let create_res = http_client 443 443 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 557 557 async fn test_oauth_2fa_lockout() { 558 558 let url = base_url().await; 559 559 let http_client = client(); 560 - let ts = Utc::now().timestamp_millis(); 561 - let handle = format!("2fa-lockout-{}", ts); 562 - let email = format!("2fa-lockout-{}@example.com", ts); 560 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 561 + let handle = format!("fl{}", suffix); 562 + let email = format!("fl{}@example.com", suffix); 563 563 let password = "Twofa123test!"; 564 564 let create_res = http_client 565 565 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 649 649 async fn test_account_selector_with_2fa() { 650 650 let url = base_url().await; 651 651 let http_client = client(); 652 - let ts = Utc::now().timestamp_millis(); 653 - let handle = format!("selector-2fa-{}", ts); 654 - let email = format!("selector-2fa-{}@example.com", ts); 652 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 653 + let handle = format!("sf{}", suffix); 654 + let email = format!("sf{}@example.com", suffix); 655 655 let password = "Selector2fa123!"; 656 656 let create_res = http_client 657 657 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 835 835 async fn test_oauth_state_encoding() { 836 836 let url = base_url().await; 837 837 let http_client = client(); 838 - let ts = Utc::now().timestamp_millis(); 839 - let handle = format!("state-special-{}", ts); 840 - let email = format!("state-special-{}@example.com", ts); 838 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 839 + let handle = format!("ss{}", suffix); 840 + let email = format!("ss{}@example.com", suffix); 841 841 let password = "State123special!"; 842 842 let create_res = http_client 843 843 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 914 914 async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) { 915 915 let url = base_url().await; 916 916 let http_client = client(); 917 - let ts = Utc::now().timestamp_millis(); 918 - let handle = format!("scope-test-{}", ts); 919 - let email = format!("scope-test-{}@example.com", ts); 917 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 918 + let handle = format!("st{}", suffix); 919 + let email = format!("st{}@example.com", suffix); 920 920 let password = "Scopetest123!"; 921 921 let create_res = http_client 922 922 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+10 -10
tests/oauth_lifecycle.rs
··· 54 54 ) -> (OAuthSession, MockServer) { 55 55 let url = base_url().await; 56 56 let http_client = client(); 57 - let ts = Utc::now().timestamp_millis(); 58 - let handle = format!("{}-{}", handle_prefix, ts); 59 - let email = format!("{}-{}@example.com", handle_prefix, ts); 57 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; 58 + let handle = format!("{}{}", handle_prefix, suffix); 59 + let email = format!("{}{}@example.com", handle_prefix, suffix); 60 60 let password = format!("{}Pass123!", handle_prefix); 61 61 let create_res = http_client 62 62 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 269 269 let url = base_url().await; 270 270 let http_client = client(); 271 271 let (session, _mock) = 272 - create_user_and_oauth_session("oauth-lifecycle", "https://example.com/callback").await; 272 + create_user_and_oauth_session("oauthlife", "https://example.com/callback").await; 273 273 let collection = "app.bsky.feed.post"; 274 274 let original_text = "Original post content"; 275 275 let create_res = http_client ··· 439 439 let url = base_url().await; 440 440 let http_client = client(); 441 441 let (session, _mock) = 442 - create_user_and_oauth_session("oauth-refresh-access", "https://example.com/callback").await; 442 + create_user_and_oauth_session("oauth-refr", "https://example.com/callback").await; 443 443 let collection = "app.bsky.feed.post"; 444 444 let create_res = http_client 445 445 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 520 520 let url = base_url().await; 521 521 let http_client = client(); 522 522 let (session, _mock) = 523 - create_user_and_oauth_session("oauth-revoke-access", "https://example.com/callback").await; 523 + create_user_and_oauth_session("oauth-revo", "https://example.com/callback").await; 524 524 let collection = "app.bsky.feed.post"; 525 525 let create_res = http_client 526 526 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 574 574 async fn test_oauth_multiple_clients_same_user() { 575 575 let url = base_url().await; 576 576 let http_client = client(); 577 - let ts = Utc::now().timestamp_millis(); 578 - let handle = format!("multi-client-{}", ts); 579 - let email = format!("multi-client-{}@example.com", ts); 577 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 578 + let handle = format!("mc{}", suffix); 579 + let email = format!("mc{}@example.com", suffix); 580 580 let password = "MultiClient123!"; 581 581 let create_res = http_client 582 582 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 949 949 let url = base_url().await; 950 950 let http_client = client(); 951 951 let (alice, _mock_alice) = 952 - create_user_and_oauth_session("alice-isolation", "https://alice.example.com/callback") 952 + create_user_and_oauth_session("alice-isol", "https://alice.example.com/callback") 953 953 .await; 954 954 let (bob, _mock_bob) = 955 955 create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await;
+13 -13
tests/oauth_scopes.rs
··· 58 58 ) -> (OAuthSession, MockServer) { 59 59 let url = base_url().await; 60 60 let http_client = client(); 61 - let ts = Utc::now().timestamp_millis(); 62 - let handle = format!("{}-{}", handle_prefix, ts); 63 - let email = format!("{}-{}@example.com", handle_prefix, ts); 61 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; 62 + let handle = format!("{}{}", handle_prefix, suffix); 63 + let email = format!("{}{}@example.com", handle_prefix, suffix); 64 64 let password = format!("{}Pass123!", handle_prefix); 65 65 66 66 let create_res = http_client ··· 345 345 let url = base_url().await; 346 346 let http_client = client(); 347 347 let (session, _mock) = create_user_and_oauth_session_with_scope( 348 - "scope-transition", 348 + "scope-trans", 349 349 "https://example.com/callback", 350 350 "atproto transition:generic", 351 351 ) ··· 380 380 let url = base_url().await; 381 381 let http_client = client(); 382 382 383 - let ts = Utc::now().timestamp_millis(); 384 - let handle = format!("consent-test-{}", ts); 385 - let email = format!("consent-{}@example.com", ts); 383 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 384 + let handle = format!("ct{}", suffix); 385 + let email = format!("ct{}@example.com", suffix); 386 386 let password = "Consent123!"; 387 387 let redirect_uri = "https://consent-test.example.com/callback"; 388 388 ··· 476 476 let url = base_url().await; 477 477 let http_client = client(); 478 478 479 - let ts = Utc::now().timestamp_millis(); 480 - let handle = format!("consent-post-{}", ts); 481 - let email = format!("consent-post-{}@example.com", ts); 479 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 480 + let handle = format!("cp{}", suffix); 481 + let email = format!("cp{}@example.com", suffix); 482 482 let password = "ConsentPost123!"; 483 483 let redirect_uri = "https://consent-post.example.com/callback"; 484 484 ··· 590 590 let url = base_url().await; 591 591 let http_client = client(); 592 592 593 - let ts = Utc::now().timestamp_millis(); 594 - let handle = format!("consent-req-{}", ts); 595 - let email = format!("consent-req-{}@example.com", ts); 593 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 594 + let handle = format!("cq{}", suffix); 595 + let email = format!("cq{}@example.com", suffix); 596 596 let password = "ConsentReq123!"; 597 597 let redirect_uri = "https://consent-req.example.com/callback"; 598 598
+12 -12
tests/oauth_security.rs
··· 41 41 } 42 42 43 43 async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) { 44 - let ts = Utc::now().timestamp_millis(); 45 - let handle = format!("sec-test-{}", ts); 44 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 45 + let handle = format!("se{}", suffix); 46 46 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 47 47 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" })) 48 48 .send().await.unwrap(); ··· 255 255 StatusCode::BAD_REQUEST, 256 256 "Missing PKCE challenge should be rejected" 257 257 ); 258 - let ts = Utc::now().timestamp_millis(); 259 - let handle = format!("pkce-attack-{}", ts); 258 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 259 + let handle = format!("pa{}", suffix); 260 260 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 261 261 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" })) 262 262 .send().await.unwrap(); ··· 326 326 async fn test_replay_attacks() { 327 327 let url = base_url().await; 328 328 let http_client = client(); 329 - let ts = Utc::now().timestamp_millis(); 330 - let handle = format!("replay-{}", ts); 329 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 330 + let handle = format!("rp{}", suffix); 331 331 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 332 332 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" })) 333 333 .send().await.unwrap(); ··· 532 532 StatusCode::BAD_REQUEST, 533 533 "Unregistered redirect_uri should be rejected" 534 534 ); 535 - let ts = Utc::now().timestamp_millis(); 536 - let handle = format!("deact-{}", ts); 535 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 536 + let handle = format!("da{}", suffix); 537 537 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 538 538 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" })) 539 539 .send().await.unwrap(); ··· 576 576 let client_id_a = mock_a.uri(); 577 577 let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await; 578 578 let client_id_b = mock_b.uri(); 579 - let ts2 = Utc::now().timestamp_millis(); 580 - let handle2 = format!("cross-{}", ts2); 579 + let suffix2 = &uuid::Uuid::new_v4().simple().to_string()[..8]; 580 + let handle2 = format!("cr{}", suffix2); 581 581 let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 582 582 .json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" })) 583 583 .send().await.unwrap(); ··· 1110 1110 async fn test_delegation_viewer_scope_cannot_write() { 1111 1111 let url = base_url().await; 1112 1112 let http_client = client(); 1113 - let ts = Utc::now().timestamp_millis(); 1113 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 1114 1114 1115 1115 let (controller_jwt, controller_did) = create_account_and_login(&http_client).await; 1116 1116 1117 - let delegated_handle = format!("deleg-{}", ts); 1117 + let delegated_handle = format!("dg{}", suffix); 1118 1118 let delegated_res = http_client 1119 1119 .post(format!( 1120 1120 "{}/xrpc/com.tranquil.delegation.createDelegatedAccount",
+5 -5
tests/password_reset.rs
··· 9 9 let client = common::client(); 10 10 let base_url = common::base_url().await; 11 11 let pool = common::get_test_db_pool().await; 12 - let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); 12 + let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 13 13 let email = format!("{}@example.com", handle); 14 14 let payload = json!({ 15 15 "handle": handle, ··· 71 71 let client = common::client(); 72 72 let base_url = common::base_url().await; 73 73 let pool = common::get_test_db_pool().await; 74 - let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); 74 + let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 75 75 let email = format!("{}@example.com", handle); 76 76 let old_password = "Oldpass123!"; 77 77 let new_password = "Newpass456!"; ··· 187 187 let client = common::client(); 188 188 let base_url = common::base_url().await; 189 189 let pool = common::get_test_db_pool().await; 190 - let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); 190 + let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 191 191 let email = format!("{}@example.com", handle); 192 192 let payload = json!({ 193 193 "handle": handle, ··· 251 251 let client = common::client(); 252 252 let base_url = common::base_url().await; 253 253 let pool = common::get_test_db_pool().await; 254 - let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); 254 + let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 255 255 let email = format!("{}@example.com", handle); 256 256 let payload = json!({ 257 257 "handle": handle, ··· 341 341 let pool = common::get_test_db_pool().await; 342 342 let client = common::client(); 343 343 let base_url = common::base_url().await; 344 - let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); 344 + let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 345 345 let email = format!("{}@example.com", handle); 346 346 let payload = json!({ 347 347 "handle": handle,
+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!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 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
··· 164 164 assert_eq!(res.status(), StatusCode::OK); 165 165 let body: Value = res.json().await.unwrap(); 166 166 let signing_key = body["signingKey"].as_str().unwrap(); 167 - let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); 167 + let handle = format!("rk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 168 168 let res = client 169 169 .post(format!( 170 170 "{}/xrpc/com.atproto.server.createAccount", ··· 202 202 async fn test_create_account_with_invalid_signing_key() { 203 203 let client = common::client(); 204 204 let base_url = common::base_url().await; 205 - let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); 205 + let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 206 206 let res = client 207 207 .post(format!( 208 208 "{}/xrpc/com.atproto.server.createAccount", ··· 238 238 assert_eq!(res.status(), StatusCode::OK); 239 239 let body: Value = res.json().await.unwrap(); 240 240 let signing_key = body["signingKey"].as_str().unwrap(); 241 - let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); 241 + let handle1 = format!("r1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 242 242 let res = client 243 243 .post(format!( 244 244 "{}/xrpc/com.atproto.server.createAccount", ··· 254 254 .await 255 255 .expect("Failed to create first account"); 256 256 assert_eq!(res.status(), StatusCode::OK); 257 - let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); 257 + let handle2 = format!("r2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 258 258 let res = client 259 259 .post(format!( 260 260 "{}/xrpc/com.atproto.server.createAccount", ··· 291 291 assert_eq!(res.status(), StatusCode::OK); 292 292 let body: Value = res.json().await.unwrap(); 293 293 let signing_key = body["signingKey"].as_str().unwrap(); 294 - let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); 294 + let handle = format!("tu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 295 295 let res = client 296 296 .post(format!( 297 297 "{}/xrpc/com.atproto.server.createAccount",
+165 -2
tests/sync_deprecated.rs
··· 62 62 .expect("Failed to send request"); 63 63 assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 64 64 let error_body: Value = not_found_res.json().await.unwrap(); 65 - assert_eq!(error_body["error"], "HeadNotFound"); 65 + assert_eq!(error_body["error"], "RepoNotFound"); 66 66 let missing_res = client 67 67 .get(format!( 68 68 "{}/xrpc/com.atproto.sync.getHead", ··· 165 165 .send() 166 166 .await 167 167 .expect("Failed to send request"); 168 - assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND); 168 + assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 169 169 let error_body: Value = not_found_res.json().await.unwrap(); 170 170 assert_eq!(error_body["error"], "RepoNotFound"); 171 171 let missing_res = client ··· 188 188 .expect("Failed to send request"); 189 189 assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST); 190 190 } 191 + 192 + #[tokio::test] 193 + async fn test_get_head_deactivated_account_returns_error() { 194 + let client = client(); 195 + let base = base_url().await; 196 + let (did, jwt) = setup_new_user("deactheadtest").await; 197 + let res = client 198 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 199 + .query(&[("did", did.as_str())]) 200 + .send() 201 + .await 202 + .unwrap(); 203 + assert_eq!(res.status(), StatusCode::OK); 204 + client 205 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 206 + .bearer_auth(&jwt) 207 + .json(&serde_json::json!({})) 208 + .send() 209 + .await 210 + .unwrap(); 211 + let deact_res = client 212 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 213 + .query(&[("did", did.as_str())]) 214 + .send() 215 + .await 216 + .unwrap(); 217 + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); 218 + let body: Value = deact_res.json().await.unwrap(); 219 + assert_eq!(body["error"], "RepoDeactivated"); 220 + } 221 + 222 + #[tokio::test] 223 + async fn test_get_head_takendown_account_returns_error() { 224 + let client = client(); 225 + let base = base_url().await; 226 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 227 + let (_, target_did) = create_account_and_login(&client).await; 228 + let res = client 229 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 230 + .query(&[("did", target_did.as_str())]) 231 + .send() 232 + .await 233 + .unwrap(); 234 + assert_eq!(res.status(), StatusCode::OK); 235 + client 236 + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) 237 + .bearer_auth(&admin_jwt) 238 + .json(&serde_json::json!({ 239 + "subject": { 240 + "$type": "com.atproto.admin.defs#repoRef", 241 + "did": target_did 242 + }, 243 + "takedown": { 244 + "applied": true, 245 + "ref": "test-takedown" 246 + } 247 + })) 248 + .send() 249 + .await 250 + .unwrap(); 251 + let takedown_res = client 252 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 253 + .query(&[("did", target_did.as_str())]) 254 + .send() 255 + .await 256 + .unwrap(); 257 + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); 258 + let body: Value = takedown_res.json().await.unwrap(); 259 + assert_eq!(body["error"], "RepoTakendown"); 260 + } 261 + 262 + #[tokio::test] 263 + async fn test_get_head_admin_can_access_deactivated() { 264 + let client = client(); 265 + let base = base_url().await; 266 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 267 + let (user_jwt, did) = create_account_and_login(&client).await; 268 + client 269 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 270 + .bearer_auth(&user_jwt) 271 + .json(&serde_json::json!({})) 272 + .send() 273 + .await 274 + .unwrap(); 275 + let res = client 276 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 277 + .bearer_auth(&admin_jwt) 278 + .query(&[("did", did.as_str())]) 279 + .send() 280 + .await 281 + .unwrap(); 282 + assert_eq!(res.status(), StatusCode::OK); 283 + } 284 + 285 + #[tokio::test] 286 + async fn test_get_checkout_deactivated_account_returns_error() { 287 + let client = client(); 288 + let base = base_url().await; 289 + let (did, jwt) = setup_new_user("deactcheckouttest").await; 290 + let res = client 291 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 292 + .query(&[("did", did.as_str())]) 293 + .send() 294 + .await 295 + .unwrap(); 296 + assert_eq!(res.status(), StatusCode::OK); 297 + client 298 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 299 + .bearer_auth(&jwt) 300 + .json(&serde_json::json!({})) 301 + .send() 302 + .await 303 + .unwrap(); 304 + let deact_res = client 305 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 306 + .query(&[("did", did.as_str())]) 307 + .send() 308 + .await 309 + .unwrap(); 310 + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); 311 + let body: Value = deact_res.json().await.unwrap(); 312 + assert_eq!(body["error"], "RepoDeactivated"); 313 + } 314 + 315 + #[tokio::test] 316 + async fn test_get_checkout_takendown_account_returns_error() { 317 + let client = client(); 318 + let base = base_url().await; 319 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 320 + let (_, target_did) = create_account_and_login(&client).await; 321 + let res = client 322 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 323 + .query(&[("did", target_did.as_str())]) 324 + .send() 325 + .await 326 + .unwrap(); 327 + assert_eq!(res.status(), StatusCode::OK); 328 + client 329 + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) 330 + .bearer_auth(&admin_jwt) 331 + .json(&serde_json::json!({ 332 + "subject": { 333 + "$type": "com.atproto.admin.defs#repoRef", 334 + "did": target_did 335 + }, 336 + "takedown": { 337 + "applied": true, 338 + "ref": "test-takedown" 339 + } 340 + })) 341 + .send() 342 + .await 343 + .unwrap(); 344 + let takedown_res = client 345 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 346 + .query(&[("did", target_did.as_str())]) 347 + .send() 348 + .await 349 + .unwrap(); 350 + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); 351 + let body: Value = takedown_res.json().await.unwrap(); 352 + assert_eq!(body["error"], "RepoTakendown"); 353 + }