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.

Fix regression with appview proxying

+75 -19
+13 -13
TODO.md
··· 53 53 - [ ] Rate limit 2FA attempts 54 54 - [ ] Re-auth for sensitive actions (email change, adding new auth methods) 55 55 56 - ### Private/encrypted data 57 - Records that only authorized parties can see and decrypt. Requires key federation between PDSes. 58 - 59 - - [ ] Survey current ATProto discourse on private data 60 - - [ ] Document Bluesky team's likely approach 61 - - [ ] Design key management strategy 62 - - [ ] Per-user encryption keys (separate from signing keys) 63 - - [ ] Key derivation for per-record or per-collection encryption 64 - - [ ] Encrypted record storage format 65 - - [ ] Transparent encryption/decryption in repo operations 66 - - [ ] Protocol for sharing decryption keys between PDSes 67 - - [ ] Handle key rotation and revocation 68 - 69 56 ### Plugin system 70 57 Extensible architecture allowing third-party plugins to add functionality, like minecraft mods or browser extensions. 71 58 ··· 81 68 - [ ] Plugin SDK crate with traits and helpers 82 69 - [ ] Example plugins: custom feed algorithm, content filter, S3 backup 83 70 - [ ] Plugin registry with signature verification and version compatibility 71 + 72 + ### Plugin: Private/encrypted data 73 + Records that only authorized parties can see and decrypt. Requires key federation between PDSes. Implemented as a plugin using the plugin system above. 74 + 75 + - [ ] Survey current ATProto discourse on private data 76 + - [ ] Document Bluesky team's likely approach 77 + - [ ] Design key management strategy 78 + - [ ] Per-user encryption keys (separate from signing keys) 79 + - [ ] Key derivation for per-record or per-collection encryption 80 + - [ ] Encrypted record storage format 81 + - [ ] Transparent encryption/decryption in repo operations 82 + - [ ] Protocol for sharing decryption keys between PDSes 83 + - [ ] Handle key rotation and revocation 84 84 85 85 --- 86 86
-1
docker-compose.prod.yml
··· 21 21 JWT_SECRET: "${JWT_SECRET:?JWT_SECRET is required (min 32 chars)}" 22 22 DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" 23 23 MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" 24 - APPVIEW_URL: "${APPVIEW_URL:-https://api.bsky.app}" 25 24 CRAWLERS: "${CRAWLERS:-https://bsky.network}" 26 25 FRONTEND_DIR: "/app/frontend/dist" 27 26 depends_on:
+1 -1
docs/install-alpine.md
··· 141 141 . /etc/bspds/bspds.env 142 142 export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL 143 143 export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY 144 - export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY APPVIEW_URL CRAWLERS 144 + export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS 145 145 } 146 146 EOF 147 147 chmod +x /etc/init.d/bspds
-1
docs/install-kubernetes.md
··· 15 15 - `VALKEY_URL` - redis:// connection string 16 16 - `PDS_HOSTNAME` - your PDS hostname (without protocol) 17 17 - `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48` 18 - - `APPVIEW_URL` - typically `https://api.bsky.app` 19 18 - `CRAWLERS` - typically `https://bsky.network` 20 19 and more, check the .env.example. 21 20
-1
scripts/install-debian.sh
··· 389 389 DPOP_SECRET=${DPOP_SECRET} 390 390 MASTER_KEY=${MASTER_KEY} 391 391 PLC_DIRECTORY_URL=https://plc.directory 392 - APPVIEW_URL=https://api.bsky.app 393 392 CRAWLERS=https://bsky.network 394 393 AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} 395 394 MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
+61 -2
src/api/repo/record/read.rs
··· 1 + use crate::api::proxy_client::proxy_client; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, 4 5 extract::{Query, State}, 5 - http::StatusCode, 6 + http::{HeaderMap, StatusCode}, 6 7 response::{IntoResponse, Response}, 7 8 }; 8 9 use cid::Cid; ··· 11 12 use serde_json::json; 12 13 use std::collections::HashMap; 13 14 use std::str::FromStr; 14 - use tracing::error; 15 + use tracing::{error, info}; 15 16 16 17 #[derive(Deserialize)] 17 18 pub struct GetRecordInput { ··· 23 24 24 25 pub async fn get_record( 25 26 State(state): State<AppState>, 27 + headers: HeaderMap, 26 28 Query(input): Query<GetRecordInput>, 27 29 ) -> Response { 28 30 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 46 48 let user_id: uuid::Uuid = match user_id_opt { 47 49 Ok(Some(id)) => id, 48 50 Ok(None) => { 51 + if let Some(proxy_header) = headers 52 + .get("atproto-proxy") 53 + .and_then(|h| h.to_str().ok()) 54 + { 55 + let did = proxy_header.split('#').next().unwrap_or(proxy_header); 56 + if let Some(resolved) = state.did_resolver.resolve_did(did).await { 57 + let mut url = format!( 58 + "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}", 59 + resolved.url.trim_end_matches('/'), 60 + urlencoding::encode(&input.repo), 61 + urlencoding::encode(&input.collection), 62 + urlencoding::encode(&input.rkey) 63 + ); 64 + if let Some(cid) = &input.cid { 65 + url.push_str(&format!("&cid={}", urlencoding::encode(cid))); 66 + } 67 + info!("Proxying getRecord to {}: {}", did, url); 68 + match proxy_client().get(&url).send().await { 69 + Ok(resp) => { 70 + let status = resp.status(); 71 + let body = match resp.bytes().await { 72 + Ok(b) => b, 73 + Err(e) => { 74 + error!("Error reading proxy response: {:?}", e); 75 + return ( 76 + StatusCode::BAD_GATEWAY, 77 + Json(json!({"error": "UpstreamFailure", "message": "Error reading upstream response"})), 78 + ) 79 + .into_response(); 80 + } 81 + }; 82 + return Response::builder() 83 + .status(status) 84 + .header("content-type", "application/json") 85 + .body(axum::body::Body::from(body)) 86 + .unwrap_or_else(|_| { 87 + (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() 88 + }); 89 + } 90 + Err(e) => { 91 + error!("Error proxying request: {:?}", e); 92 + return ( 93 + StatusCode::BAD_GATEWAY, 94 + Json(json!({"error": "UpstreamFailure", "message": "Failed to reach upstream service"})), 95 + ) 96 + .into_response(); 97 + } 98 + } 99 + } else { 100 + error!("Could not resolve DID from atproto-proxy header: {}", did); 101 + return ( 102 + StatusCode::BAD_GATEWAY, 103 + Json(json!({"error": "UpstreamFailure", "message": "Could not resolve proxy DID"})), 104 + ) 105 + .into_response(); 106 + } 107 + } 49 108 return ( 50 109 StatusCode::NOT_FOUND, 51 110 Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),