this repo has no description

Fixing my whoopsies

lewis b846f6f1 d01c1445

Changed files
+454 -114
frontend
src
scripts
src
+7
Cargo.lock
··· 744 744 ] 745 745 746 746 [[package]] 747 + name = "base32" 748 + version = "0.5.1" 749 + source = "registry+https://github.com/rust-lang/crates.io-index" 750 + checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" 751 + 752 + [[package]] 747 753 name = "base64" 748 754 version = "0.21.7" 749 755 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 933 939 "aws-config", 934 940 "aws-sdk-s3", 935 941 "axum", 942 + "base32", 936 943 "base64 0.22.1", 937 944 "bcrypt", 938 945 "bytes",
+2 -1
Cargo.toml
··· 9 9 aws-config = "1.8.11" 10 10 aws-sdk-s3 = "1.116.0" 11 11 axum = { version = "0.8.7", features = ["ws", "macros"] } 12 + base32 = "0.5" 12 13 base64 = "0.22.1" 13 14 bcrypt = "0.17.1" 14 15 bytes = "1.11.0" ··· 50 51 iroh-car = "0.5.1" 51 52 image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } 52 53 redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] } 53 - tower-http = { version = "0.6", features = ["fs"] } 54 + tower-http = { version = "0.6", features = ["fs", "cors"] } 54 55 metrics = "0.24" 55 56 metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } 56 57
+9
frontend/src/routes/Register.svelte
··· 79 79 80 80 async function handleSubmit(e: Event) { 81 81 e.preventDefault() 82 + console.log('[Register] handleSubmit called') 82 83 83 84 const validationError = validateForm() 84 85 if (validationError) { 86 + console.log('[Register] validation error:', validationError) 85 87 error = validationError 86 88 return 87 89 } 88 90 89 91 submitting = true 90 92 error = null 93 + console.log('[Register] starting registration...') 91 94 92 95 try { 93 96 const result = await register({ ··· 100 103 telegramUsername: telegramUsername.trim() || undefined, 101 104 signalNumber: signalNumber.trim() || undefined, 102 105 }) 106 + console.log('[Register] registration result:', result) 103 107 104 108 if (result.verificationRequired) { 109 + console.log('[Register] setting pendingVerification') 105 110 pendingVerification = { 106 111 did: result.did, 107 112 handle: result.handle, 108 113 channel: result.verificationChannel, 109 114 } 115 + console.log('[Register] pendingVerification set to:', pendingVerification) 110 116 } else { 117 + console.log('[Register] no verification required, navigating to dashboard') 111 118 navigate('/dashboard') 112 119 } 113 120 } catch (err: any) { 121 + console.error('[Register] error:', err) 114 122 if (err instanceof ApiError) { 115 123 error = err.message || 'Registration failed' 116 124 } else if (err instanceof Error) { ··· 120 128 } 121 129 } finally { 122 130 submitting = false 131 + console.log('[Register] finished, submitting=false') 123 132 } 124 133 } 125 134
+92
scripts/install-debian.sh
··· 22 22 log_warn "This script is designed for Debian. Proceed with caution on other distros." 23 23 fi 24 24 25 + nuke_installation() { 26 + echo -e "${RED}" 27 + echo "╔═══════════════════════════════════════════════════════════════════╗" 28 + echo "║ NUKING EXISTING INSTALLATION ║" 29 + echo "╚═══════════════════════════════════════════════════════════════════╝" 30 + echo -e "${NC}" 31 + 32 + log_info "Stopping services..." 33 + systemctl stop bspds 2>/dev/null || true 34 + systemctl disable bspds 2>/dev/null || true 35 + 36 + log_info "Removing BSPDS files..." 37 + rm -rf /opt/bspds 38 + rm -rf /var/lib/bspds 39 + rm -f /usr/local/bin/bspds 40 + rm -f /usr/local/bin/bspds-sendmail 41 + rm -f /usr/local/bin/bspds-mailq 42 + rm -rf /var/spool/bspds-mail 43 + rm -f /etc/systemd/system/bspds.service 44 + systemctl daemon-reload 45 + 46 + log_info "Removing BSPDS configuration..." 47 + rm -rf /etc/bspds 48 + 49 + log_info "Dropping postgres database and user..." 50 + sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true 51 + sudo -u postgres psql -c "DROP USER IF EXISTS bspds;" 2>/dev/null || true 52 + 53 + log_info "Removing minio bucket and resetting minio..." 54 + if command -v mc &>/dev/null; then 55 + mc rb local/pds-blobs --force 2>/dev/null || true 56 + mc alias remove local 2>/dev/null || true 57 + fi 58 + systemctl stop minio 2>/dev/null || true 59 + rm -rf /var/lib/minio/data/.minio.sys 2>/dev/null || true 60 + rm -f /etc/default/minio 2>/dev/null || true 61 + 62 + log_info "Removing nginx config..." 63 + rm -f /etc/nginx/sites-enabled/bspds 64 + rm -f /etc/nginx/sites-available/bspds 65 + systemctl reload nginx 2>/dev/null || true 66 + 67 + log_success "Previous installation nuked!" 68 + echo "" 69 + } 70 + 71 + if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/bspds ]]; then 72 + echo -e "${YELLOW}" 73 + echo "╔═══════════════════════════════════════════════════════════════════╗" 74 + echo "║ EXISTING INSTALLATION DETECTED ║" 75 + echo "╚═══════════════════════════════════════════════════════════════════╝" 76 + echo -e "${NC}" 77 + echo "" 78 + echo "Options:" 79 + echo " 1) Nuke everything and start fresh (destroys database!)" 80 + echo " 2) Continue with existing installation (idempotent update)" 81 + echo " 3) Exit" 82 + echo "" 83 + read -p "Choose an option [1/2/3]: " INSTALL_CHOICE 84 + 85 + case "$INSTALL_CHOICE" in 86 + 1) 87 + echo "" 88 + echo -e "${RED}WARNING: This will DELETE:${NC}" 89 + echo " - PostgreSQL database 'pds' and all data" 90 + echo " - All BSPDS configuration and credentials" 91 + echo " - All source code in /opt/bspds" 92 + echo " - MinIO bucket 'pds-blobs' and all blobs" 93 + echo " - Mail queue contents" 94 + echo "" 95 + read -p "Type 'NUKE' to confirm destruction: " CONFIRM_NUKE 96 + if [[ "$CONFIRM_NUKE" == "NUKE" ]]; then 97 + nuke_installation 98 + else 99 + log_error "Nuke cancelled. Exiting." 100 + exit 1 101 + fi 102 + ;; 103 + 2) 104 + log_info "Continuing with existing installation..." 105 + ;; 106 + 3) 107 + log_info "Exiting." 108 + exit 0 109 + ;; 110 + *) 111 + log_error "Invalid option. Exiting." 112 + exit 1 113 + ;; 114 + esac 115 + fi 116 + 25 117 echo -e "${CYAN}" 26 118 echo "╔═══════════════════════════════════════════════════════════════════╗" 27 119 echo "║ BSPDS Installation Script for Debian ║"
+8 -1
src/api/feed/actor_likes.rs
··· 122 122 let actor_did = if params.actor.starts_with("did:") { 123 123 params.actor.clone() 124 124 } else { 125 - match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", params.actor) 125 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 126 + let suffix = format!(".{}", hostname); 127 + let short_handle = if params.actor.ends_with(&suffix) { 128 + params.actor.strip_suffix(&suffix).unwrap_or(&params.actor) 129 + } else { 130 + &params.actor 131 + }; 132 + match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", short_handle) 126 133 .fetch_optional(&state.db) 127 134 .await 128 135 {
+8 -1
src/api/feed/author_feed.rs
··· 104 104 let actor_did = if params.actor.starts_with("did:") { 105 105 params.actor.clone() 106 106 } else { 107 - match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", params.actor) 107 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 108 + let suffix = format!(".{}", hostname); 109 + let short_handle = if params.actor.ends_with(&suffix) { 110 + params.actor.strip_suffix(&suffix).unwrap_or(&params.actor) 111 + } else { 112 + &params.actor 113 + }; 114 + match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", short_handle) 108 115 .fetch_optional(&state.db) 109 116 .await 110 117 {
+181 -104
src/api/identity/account.rs
··· 1 1 use super::did::verify_did_web; 2 + use crate::plc::{create_genesis_operation, signing_key_to_did_key, PlcClient}; 2 3 use crate::state::{AppState, RateLimitKind}; 3 4 use axum::{ 4 5 Json, ··· 99 100 } 100 101 } 101 102 103 + let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 104 + let valid_channels = ["email", "discord", "telegram", "signal"]; 105 + if !valid_channels.contains(&verification_channel) { 106 + return ( 107 + StatusCode::BAD_REQUEST, 108 + Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})), 109 + ) 110 + .into_response(); 111 + } 112 + 113 + let verification_recipient = match verification_channel { 114 + "email" => match &input.email { 115 + Some(email) if !email.trim().is_empty() => email.trim().to_string(), 116 + _ => return ( 117 + StatusCode::BAD_REQUEST, 118 + Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 119 + ).into_response(), 120 + }, 121 + "discord" => match &input.discord_id { 122 + Some(id) if !id.trim().is_empty() => id.trim().to_string(), 123 + _ => return ( 124 + StatusCode::BAD_REQUEST, 125 + Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 126 + ).into_response(), 127 + }, 128 + "telegram" => match &input.telegram_username { 129 + Some(username) if !username.trim().is_empty() => username.trim().to_string(), 130 + _ => return ( 131 + StatusCode::BAD_REQUEST, 132 + Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 133 + ).into_response(), 134 + }, 135 + "signal" => match &input.signal_number { 136 + Some(number) if !number.trim().is_empty() => number.trim().to_string(), 137 + _ => return ( 138 + StatusCode::BAD_REQUEST, 139 + Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 140 + ).into_response(), 141 + }, 142 + _ => return ( 143 + StatusCode::BAD_REQUEST, 144 + Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 145 + ).into_response(), 146 + }; 147 + 148 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 149 + let pds_endpoint = format!("https://{}", hostname); 150 + let full_handle = format!("{}.{}", input.handle, hostname); 151 + 152 + let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 153 + if let Some(signing_key_did) = &input.signing_key { 154 + let reserved = sqlx::query!( 155 + r#" 156 + SELECT id, private_key_bytes 157 + FROM reserved_signing_keys 158 + WHERE public_key_did_key = $1 159 + AND used_at IS NULL 160 + AND expires_at > NOW() 161 + FOR UPDATE 162 + "#, 163 + signing_key_did 164 + ) 165 + .fetch_optional(&state.db) 166 + .await; 167 + 168 + match reserved { 169 + Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 170 + Ok(None) => { 171 + return ( 172 + StatusCode::BAD_REQUEST, 173 + Json(json!({ 174 + "error": "InvalidSigningKey", 175 + "message": "Signing key not found, already used, or expired" 176 + })), 177 + ) 178 + .into_response(); 179 + } 180 + Err(e) => { 181 + error!("Error looking up reserved signing key: {:?}", e); 182 + return ( 183 + StatusCode::INTERNAL_SERVER_ERROR, 184 + Json(json!({"error": "InternalError"})), 185 + ) 186 + .into_response(); 187 + } 188 + } 189 + } else { 190 + let secret_key = SecretKey::random(&mut OsRng); 191 + (secret_key.to_bytes().to_vec(), None) 192 + }; 193 + 194 + let signing_key = match SigningKey::from_slice(&secret_key_bytes) { 195 + Ok(k) => k, 196 + Err(e) => { 197 + error!("Error creating signing key: {:?}", e); 198 + return ( 199 + StatusCode::INTERNAL_SERVER_ERROR, 200 + Json(json!({"error": "InternalError"})), 201 + ) 202 + .into_response(); 203 + } 204 + }; 205 + 102 206 let did = if let Some(d) = &input.did { 103 207 if d.trim().is_empty() { 104 - format!("did:plc:{}", uuid::Uuid::new_v4()) 105 - } else { 106 - let hostname = 107 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 208 + let rotation_key = std::env::var("PLC_ROTATION_KEY") 209 + .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 210 + 211 + let genesis_result = match create_genesis_operation( 212 + &signing_key, 213 + &rotation_key, 214 + &full_handle, 215 + &pds_endpoint, 216 + ) { 217 + Ok(r) => r, 218 + Err(e) => { 219 + error!("Error creating PLC genesis operation: {:?}", e); 220 + return ( 221 + StatusCode::INTERNAL_SERVER_ERROR, 222 + Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 223 + ) 224 + .into_response(); 225 + } 226 + }; 227 + 228 + let plc_client = PlcClient::new(None); 229 + if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await { 230 + error!("Failed to submit PLC genesis operation: {:?}", e); 231 + return ( 232 + StatusCode::BAD_GATEWAY, 233 + Json(json!({ 234 + "error": "UpstreamError", 235 + "message": format!("Failed to register DID with PLC directory: {}", e) 236 + })), 237 + ) 238 + .into_response(); 239 + } 240 + 241 + info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 242 + genesis_result.did 243 + } else if d.starts_with("did:web:") { 108 244 if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 109 245 return ( 110 246 StatusCode::BAD_REQUEST, ··· 113 249 .into_response(); 114 250 } 115 251 d.clone() 252 + } else { 253 + return ( 254 + StatusCode::BAD_REQUEST, 255 + Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc"})), 256 + ) 257 + .into_response(); 116 258 } 117 259 } else { 118 - format!("did:plc:{}", uuid::Uuid::new_v4()) 260 + let rotation_key = std::env::var("PLC_ROTATION_KEY") 261 + .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 262 + 263 + let genesis_result = match create_genesis_operation( 264 + &signing_key, 265 + &rotation_key, 266 + &full_handle, 267 + &pds_endpoint, 268 + ) { 269 + Ok(r) => r, 270 + Err(e) => { 271 + error!("Error creating PLC genesis operation: {:?}", e); 272 + return ( 273 + StatusCode::INTERNAL_SERVER_ERROR, 274 + Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 275 + ) 276 + .into_response(); 277 + } 278 + }; 279 + 280 + let plc_client = PlcClient::new(None); 281 + if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await { 282 + error!("Failed to submit PLC genesis operation: {:?}", e); 283 + return ( 284 + StatusCode::BAD_GATEWAY, 285 + Json(json!({ 286 + "error": "UpstreamError", 287 + "message": format!("Failed to register DID with PLC directory: {}", e) 288 + })), 289 + ) 290 + .into_response(); 291 + } 292 + 293 + info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 294 + genesis_result.did 119 295 }; 120 296 121 297 let mut tx = match state.db.begin().await { ··· 211 387 } 212 388 }; 213 389 214 - let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 215 - let valid_channels = ["email", "discord", "telegram", "signal"]; 216 - if !valid_channels.contains(&verification_channel) { 217 - return ( 218 - StatusCode::BAD_REQUEST, 219 - Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})), 220 - ) 221 - .into_response(); 222 - } 223 - 224 - let verification_recipient = match verification_channel { 225 - "email" => match &input.email { 226 - Some(email) if !email.trim().is_empty() => email.trim().to_string(), 227 - _ => return ( 228 - StatusCode::BAD_REQUEST, 229 - Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 230 - ).into_response(), 231 - }, 232 - "discord" => match &input.discord_id { 233 - Some(id) if !id.trim().is_empty() => id.trim().to_string(), 234 - _ => return ( 235 - StatusCode::BAD_REQUEST, 236 - Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 237 - ).into_response(), 238 - }, 239 - "telegram" => match &input.telegram_username { 240 - Some(username) if !username.trim().is_empty() => username.trim().to_string(), 241 - _ => return ( 242 - StatusCode::BAD_REQUEST, 243 - Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 244 - ).into_response(), 245 - }, 246 - "signal" => match &input.signal_number { 247 - Some(number) if !number.trim().is_empty() => number.trim().to_string(), 248 - _ => return ( 249 - StatusCode::BAD_REQUEST, 250 - Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 251 - ).into_response(), 252 - }, 253 - _ => return ( 254 - StatusCode::BAD_REQUEST, 255 - Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 256 - ).into_response(), 257 - }; 258 - 259 390 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000); 260 391 let code_expires_at = chrono::Utc::now() + chrono::Duration::minutes(30); 261 392 ··· 325 456 } 326 457 }; 327 458 328 - let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 329 - if let Some(signing_key_did) = &input.signing_key { 330 - let reserved = sqlx::query!( 331 - r#" 332 - SELECT id, private_key_bytes 333 - FROM reserved_signing_keys 334 - WHERE public_key_did_key = $1 335 - AND used_at IS NULL 336 - AND expires_at > NOW() 337 - FOR UPDATE 338 - "#, 339 - signing_key_did 340 - ) 341 - .fetch_optional(&mut *tx) 342 - .await; 343 - 344 - match reserved { 345 - Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 346 - Ok(None) => { 347 - return ( 348 - StatusCode::BAD_REQUEST, 349 - Json(json!({ 350 - "error": "InvalidSigningKey", 351 - "message": "Signing key not found, already used, or expired" 352 - })), 353 - ) 354 - .into_response(); 355 - } 356 - Err(e) => { 357 - error!("Error looking up reserved signing key: {:?}", e); 358 - return ( 359 - StatusCode::INTERNAL_SERVER_ERROR, 360 - Json(json!({"error": "InternalError"})), 361 - ) 362 - .into_response(); 363 - } 364 - } 365 - } else { 366 - let secret_key = SecretKey::random(&mut OsRng); 367 - (secret_key.to_bytes().to_vec(), None) 368 - }; 369 - 370 459 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 371 460 Ok(enc) => enc, 372 461 Err(e) => { ··· 442 531 let rev = Tid::now(LimitedU32::MIN); 443 532 444 533 let unsigned_commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 445 - 446 - let signing_key = match SigningKey::from_slice(&secret_key_bytes) { 447 - Ok(k) => k, 448 - Err(e) => { 449 - error!("Error creating signing key: {:?}", e); 450 - return ( 451 - StatusCode::INTERNAL_SERVER_ERROR, 452 - Json(json!({"error": "InternalError"})), 453 - ) 454 - .into_response(); 455 - } 456 - }; 457 534 458 535 let signed_commit = match unsigned_commit.sign(&signing_key) { 459 536 Ok(c) => c,
+44 -2
src/api/identity/did.rs
··· 3 3 use axum::{ 4 4 Json, 5 5 extract::{Path, Query, State}, 6 - http::StatusCode, 6 + http::{HeaderMap, StatusCode}, 7 7 response::{IntoResponse, Response}, 8 8 }; 9 9 use base64::Engine; ··· 38 38 return (StatusCode::OK, Json(json!({ "did": did }))).into_response(); 39 39 } 40 40 41 - let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle) 41 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 42 + let suffix = format!(".{}", hostname); 43 + let short_handle = if handle.ends_with(&suffix) { 44 + handle.strip_suffix(&suffix).unwrap_or(handle) 45 + } else { 46 + handle 47 + }; 48 + 49 + let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle) 42 50 .fetch_optional(&state.db) 43 51 .await; 44 52 ··· 452 460 } 453 461 } 454 462 } 463 + 464 + pub async fn well_known_atproto_did( 465 + State(state): State<AppState>, 466 + headers: HeaderMap, 467 + ) -> Response { 468 + let host = match headers.get("host").and_then(|h| h.to_str().ok()) { 469 + Some(h) => h, 470 + None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), 471 + }; 472 + 473 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 474 + let suffix = format!(".{}", hostname); 475 + 476 + let handle = host.split(':').next().unwrap_or(host); 477 + 478 + let short_handle = if handle.ends_with(&suffix) { 479 + handle.strip_suffix(&suffix).unwrap_or(handle) 480 + } else { 481 + return (StatusCode::NOT_FOUND, "Handle not found").into_response(); 482 + }; 483 + 484 + let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle) 485 + .fetch_optional(&state.db) 486 + .await; 487 + 488 + match user { 489 + Ok(Some(row)) => row.did.into_response(), 490 + Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(), 491 + Err(e) => { 492 + error!("DB error in well-known atproto-did: {:?}", e); 493 + (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() 494 + } 495 + } 496 + }
+1
src/api/identity/mod.rs
··· 5 5 pub use account::create_account; 6 6 pub use did::{ 7 7 get_recommended_did_credentials, resolve_handle, update_handle, user_did_doc, well_known_did, 8 + well_known_atproto_did, 8 9 }; 9 10 pub use plc::{request_plc_operation_signature, sign_plc_operation, submit_plc_operation};
+12 -3
src/api/repo/meta.rs
··· 17 17 State(state): State<AppState>, 18 18 Query(input): Query<DescribeRepoInput>, 19 19 ) -> Response { 20 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 21 + 20 22 let user_row = if input.repo.starts_with("did:") { 21 23 sqlx::query!("SELECT id, handle, did FROM users WHERE did = $1", input.repo) 22 24 .fetch_optional(&state.db) 23 25 .await 24 26 .map(|opt| opt.map(|r| (r.id, r.handle, r.did))) 25 27 } else { 26 - sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", input.repo) 28 + let suffix = format!(".{}", hostname); 29 + let short_handle = if input.repo.ends_with(&suffix) { 30 + input.repo.strip_suffix(&suffix).unwrap_or(&input.repo) 31 + } else { 32 + &input.repo 33 + }; 34 + sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", short_handle) 27 35 .fetch_optional(&state.db) 28 36 .await 29 37 .map(|opt| opt.map(|r| (r.id, r.handle, r.did))) ··· 50 58 Err(_) => Vec::new(), 51 59 }; 52 60 61 + let full_handle = format!("{}.{}", handle, hostname); 53 62 let did_doc = json!({ 54 63 "id": did, 55 - "alsoKnownAs": [format!("at://{}", handle)] 64 + "alsoKnownAs": [format!("at://{}", full_handle)] 56 65 }); 57 66 58 67 Json(json!({ 59 - "handle": handle, 68 + "handle": full_handle, 60 69 "did": did, 61 70 "didDoc": did_doc, 62 71 "collections": collections,
+18 -2
src/api/repo/record/read.rs
··· 25 25 State(state): State<AppState>, 26 26 Query(input): Query<GetRecordInput>, 27 27 ) -> Response { 28 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 29 + 28 30 let user_id_opt = if input.repo.starts_with("did:") { 29 31 sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo) 30 32 .fetch_optional(&state.db) 31 33 .await 32 34 .map(|opt| opt.map(|r| r.id)) 33 35 } else { 34 - sqlx::query!("SELECT id FROM users WHERE handle = $1", input.repo) 36 + let suffix = format!(".{}", hostname); 37 + let short_handle = if input.repo.ends_with(&suffix) { 38 + input.repo.strip_suffix(&suffix).unwrap_or(&input.repo) 39 + } else { 40 + &input.repo 41 + }; 42 + sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle) 35 43 .fetch_optional(&state.db) 36 44 .await 37 45 .map(|opt| opt.map(|r| r.id)) ··· 143 151 State(state): State<AppState>, 144 152 Query(input): Query<ListRecordsInput>, 145 153 ) -> Response { 154 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 155 + 146 156 let user_id_opt = if input.repo.starts_with("did:") { 147 157 sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo) 148 158 .fetch_optional(&state.db) 149 159 .await 150 160 .map(|opt| opt.map(|r| r.id)) 151 161 } else { 152 - sqlx::query!("SELECT id FROM users WHERE handle = $1", input.repo) 162 + let suffix = format!(".{}", hostname); 163 + let short_handle = if input.repo.ends_with(&suffix) { 164 + input.repo.strip_suffix(&suffix).unwrap_or(&input.repo) 165 + } else { 166 + &input.repo 167 + }; 168 + sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle) 153 169 .fetch_optional(&state.db) 154 170 .await 155 171 .map(|opt| opt.map(|r| r.id))
+9
src/lib.rs
··· 19 19 20 20 use axum::{ 21 21 Router, 22 + http::Method, 22 23 middleware, 23 24 routing::{any, get, post}, 24 25 }; 25 26 use state::AppState; 27 + use tower_http::cors::{Any, CorsLayer}; 26 28 use tower_http::services::{ServeDir, ServeFile}; 27 29 28 30 pub fn app(state: AppState) -> Router { ··· 348 350 post(api::notification::register_push), 349 351 ) 350 352 .route("/.well-known/did.json", get(api::identity::well_known_did)) 353 + .route("/.well-known/atproto-did", get(api::identity::well_known_atproto_did)) 351 354 .route("/u/{handle}/did.json", get(api::identity::user_did_doc)) 352 355 // OAuth 2.1 endpoints 353 356 .route( ··· 386 389 ) 387 390 .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) 388 391 .layer(middleware::from_fn(metrics::metrics_middleware)) 392 + .layer( 393 + CorsLayer::new() 394 + .allow_origin(Any) 395 + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) 396 + .allow_headers(Any), 397 + ) 389 398 .with_state(state); 390 399 391 400 let frontend_dir = std::env::var("FRONTEND_DIR")
+63
src/plc/mod.rs
··· 1 + use base32::Alphabet; 1 2 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 2 3 use k256::ecdsa::{SigningKey, Signature, signature::Signer}; 3 4 use reqwest::Client; ··· 306 307 307 308 let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed); 308 309 format!("did:key:{}", encoded) 310 + } 311 + 312 + pub struct GenesisResult { 313 + pub did: String, 314 + pub signed_operation: Value, 315 + } 316 + 317 + pub fn create_genesis_operation( 318 + signing_key: &SigningKey, 319 + rotation_key: &str, 320 + handle: &str, 321 + pds_endpoint: &str, 322 + ) -> Result<GenesisResult, PlcError> { 323 + let signing_did_key = signing_key_to_did_key(signing_key); 324 + 325 + let mut verification_methods = HashMap::new(); 326 + verification_methods.insert("atproto".to_string(), signing_did_key.clone()); 327 + 328 + let mut services = HashMap::new(); 329 + services.insert( 330 + "atproto_pds".to_string(), 331 + PlcService { 332 + service_type: "AtprotoPersonalDataServer".to_string(), 333 + endpoint: pds_endpoint.to_string(), 334 + }, 335 + ); 336 + 337 + let genesis_op = PlcOperation { 338 + op_type: "plc_operation".to_string(), 339 + rotation_keys: vec![rotation_key.to_string()], 340 + verification_methods, 341 + also_known_as: vec![format!("at://{}", handle)], 342 + services, 343 + prev: None, 344 + sig: None, 345 + }; 346 + 347 + let genesis_value = serde_json::to_value(&genesis_op) 348 + .map_err(|e| PlcError::Serialization(e.to_string()))?; 349 + 350 + let signed_op = sign_operation(&genesis_value, signing_key)?; 351 + 352 + let did = did_for_genesis_op(&signed_op)?; 353 + 354 + Ok(GenesisResult { 355 + did, 356 + signed_operation: signed_op, 357 + }) 358 + } 359 + 360 + pub fn did_for_genesis_op(signed_op: &Value) -> Result<String, PlcError> { 361 + let cbor_bytes = serde_ipld_dagcbor::to_vec(signed_op) 362 + .map_err(|e| PlcError::Serialization(e.to_string()))?; 363 + 364 + let mut hasher = Sha256::new(); 365 + hasher.update(&cbor_bytes); 366 + let hash = hasher.finalize(); 367 + 368 + let encoded = base32::encode(Alphabet::Rfc4648Lower { padding: false }, &hash); 369 + let truncated = &encoded[..24]; 370 + 371 + Ok(format!("did:plc:{}", truncated)) 309 372 } 310 373 311 374 pub fn validate_plc_operation(op: &Value) -> Result<(), PlcError> {