+13
-13
TODO.md
+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
-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
+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
-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
-1
scripts/install-debian.sh
+61
-2
src/api/repo/record/read.rs
+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"})),