+7
Cargo.lock
+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
+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
+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
+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
+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(¶ms.actor)
129
+
} else {
130
+
¶ms.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
{
+181
-104
src/api/identity/account.rs
+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
+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
+1
src/api/identity/mod.rs
+12
-3
src/api/repo/meta.rs
+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
+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
+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
+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> {