···122122 let actor_did = if params.actor.starts_with("did:") {
123123 params.actor.clone()
124124 } else {
125125- match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", params.actor)
125125+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
126126+ let suffix = format!(".{}", hostname);
127127+ let short_handle = if params.actor.ends_with(&suffix) {
128128+ params.actor.strip_suffix(&suffix).unwrap_or(¶ms.actor)
129129+ } else {
130130+ ¶ms.actor
131131+ };
132132+ match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", short_handle)
126133 .fetch_optional(&state.db)
127134 .await
128135 {
+8-1
src/api/feed/author_feed.rs
···104104 let actor_did = if params.actor.starts_with("did:") {
105105 params.actor.clone()
106106 } else {
107107- match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", params.actor)
107107+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
108108+ let suffix = format!(".{}", hostname);
109109+ let short_handle = if params.actor.ends_with(&suffix) {
110110+ params.actor.strip_suffix(&suffix).unwrap_or(¶ms.actor)
111111+ } else {
112112+ ¶ms.actor
113113+ };
114114+ match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", short_handle)
108115 .fetch_optional(&state.db)
109116 .await
110117 {
+181-104
src/api/identity/account.rs
···11use super::did::verify_did_web;
22+use crate::plc::{create_genesis_operation, signing_key_to_did_key, PlcClient};
23use crate::state::{AppState, RateLimitKind};
34use axum::{
45 Json,
···99100 }
100101 }
101102103103+ let verification_channel = input.verification_channel.as_deref().unwrap_or("email");
104104+ let valid_channels = ["email", "discord", "telegram", "signal"];
105105+ if !valid_channels.contains(&verification_channel) {
106106+ return (
107107+ StatusCode::BAD_REQUEST,
108108+ Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})),
109109+ )
110110+ .into_response();
111111+ }
112112+113113+ let verification_recipient = match verification_channel {
114114+ "email" => match &input.email {
115115+ Some(email) if !email.trim().is_empty() => email.trim().to_string(),
116116+ _ => return (
117117+ StatusCode::BAD_REQUEST,
118118+ Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})),
119119+ ).into_response(),
120120+ },
121121+ "discord" => match &input.discord_id {
122122+ Some(id) if !id.trim().is_empty() => id.trim().to_string(),
123123+ _ => return (
124124+ StatusCode::BAD_REQUEST,
125125+ Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})),
126126+ ).into_response(),
127127+ },
128128+ "telegram" => match &input.telegram_username {
129129+ Some(username) if !username.trim().is_empty() => username.trim().to_string(),
130130+ _ => return (
131131+ StatusCode::BAD_REQUEST,
132132+ Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})),
133133+ ).into_response(),
134134+ },
135135+ "signal" => match &input.signal_number {
136136+ Some(number) if !number.trim().is_empty() => number.trim().to_string(),
137137+ _ => return (
138138+ StatusCode::BAD_REQUEST,
139139+ Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})),
140140+ ).into_response(),
141141+ },
142142+ _ => return (
143143+ StatusCode::BAD_REQUEST,
144144+ Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})),
145145+ ).into_response(),
146146+ };
147147+148148+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
149149+ let pds_endpoint = format!("https://{}", hostname);
150150+ let full_handle = format!("{}.{}", input.handle, hostname);
151151+152152+ let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
153153+ if let Some(signing_key_did) = &input.signing_key {
154154+ let reserved = sqlx::query!(
155155+ r#"
156156+ SELECT id, private_key_bytes
157157+ FROM reserved_signing_keys
158158+ WHERE public_key_did_key = $1
159159+ AND used_at IS NULL
160160+ AND expires_at > NOW()
161161+ FOR UPDATE
162162+ "#,
163163+ signing_key_did
164164+ )
165165+ .fetch_optional(&state.db)
166166+ .await;
167167+168168+ match reserved {
169169+ Ok(Some(row)) => (row.private_key_bytes, Some(row.id)),
170170+ Ok(None) => {
171171+ return (
172172+ StatusCode::BAD_REQUEST,
173173+ Json(json!({
174174+ "error": "InvalidSigningKey",
175175+ "message": "Signing key not found, already used, or expired"
176176+ })),
177177+ )
178178+ .into_response();
179179+ }
180180+ Err(e) => {
181181+ error!("Error looking up reserved signing key: {:?}", e);
182182+ return (
183183+ StatusCode::INTERNAL_SERVER_ERROR,
184184+ Json(json!({"error": "InternalError"})),
185185+ )
186186+ .into_response();
187187+ }
188188+ }
189189+ } else {
190190+ let secret_key = SecretKey::random(&mut OsRng);
191191+ (secret_key.to_bytes().to_vec(), None)
192192+ };
193193+194194+ let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
195195+ Ok(k) => k,
196196+ Err(e) => {
197197+ error!("Error creating signing key: {:?}", e);
198198+ return (
199199+ StatusCode::INTERNAL_SERVER_ERROR,
200200+ Json(json!({"error": "InternalError"})),
201201+ )
202202+ .into_response();
203203+ }
204204+ };
205205+102206 let did = if let Some(d) = &input.did {
103207 if d.trim().is_empty() {
104104- format!("did:plc:{}", uuid::Uuid::new_v4())
105105- } else {
106106- let hostname =
107107- std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
208208+ let rotation_key = std::env::var("PLC_ROTATION_KEY")
209209+ .unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
210210+211211+ let genesis_result = match create_genesis_operation(
212212+ &signing_key,
213213+ &rotation_key,
214214+ &full_handle,
215215+ &pds_endpoint,
216216+ ) {
217217+ Ok(r) => r,
218218+ Err(e) => {
219219+ error!("Error creating PLC genesis operation: {:?}", e);
220220+ return (
221221+ StatusCode::INTERNAL_SERVER_ERROR,
222222+ Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})),
223223+ )
224224+ .into_response();
225225+ }
226226+ };
227227+228228+ let plc_client = PlcClient::new(None);
229229+ if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
230230+ error!("Failed to submit PLC genesis operation: {:?}", e);
231231+ return (
232232+ StatusCode::BAD_GATEWAY,
233233+ Json(json!({
234234+ "error": "UpstreamError",
235235+ "message": format!("Failed to register DID with PLC directory: {}", e)
236236+ })),
237237+ )
238238+ .into_response();
239239+ }
240240+241241+ info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
242242+ genesis_result.did
243243+ } else if d.starts_with("did:web:") {
108244 if let Err(e) = verify_did_web(d, &hostname, &input.handle).await {
109245 return (
110246 StatusCode::BAD_REQUEST,
···113249 .into_response();
114250 }
115251 d.clone()
252252+ } else {
253253+ return (
254254+ StatusCode::BAD_REQUEST,
255255+ Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc"})),
256256+ )
257257+ .into_response();
116258 }
117259 } else {
118118- format!("did:plc:{}", uuid::Uuid::new_v4())
260260+ let rotation_key = std::env::var("PLC_ROTATION_KEY")
261261+ .unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
262262+263263+ let genesis_result = match create_genesis_operation(
264264+ &signing_key,
265265+ &rotation_key,
266266+ &full_handle,
267267+ &pds_endpoint,
268268+ ) {
269269+ Ok(r) => r,
270270+ Err(e) => {
271271+ error!("Error creating PLC genesis operation: {:?}", e);
272272+ return (
273273+ StatusCode::INTERNAL_SERVER_ERROR,
274274+ Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})),
275275+ )
276276+ .into_response();
277277+ }
278278+ };
279279+280280+ let plc_client = PlcClient::new(None);
281281+ if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
282282+ error!("Failed to submit PLC genesis operation: {:?}", e);
283283+ return (
284284+ StatusCode::BAD_GATEWAY,
285285+ Json(json!({
286286+ "error": "UpstreamError",
287287+ "message": format!("Failed to register DID with PLC directory: {}", e)
288288+ })),
289289+ )
290290+ .into_response();
291291+ }
292292+293293+ info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
294294+ genesis_result.did
119295 };
120296121297 let mut tx = match state.db.begin().await {
···211387 }
212388 };
213389214214- let verification_channel = input.verification_channel.as_deref().unwrap_or("email");
215215- let valid_channels = ["email", "discord", "telegram", "signal"];
216216- if !valid_channels.contains(&verification_channel) {
217217- return (
218218- StatusCode::BAD_REQUEST,
219219- Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})),
220220- )
221221- .into_response();
222222- }
223223-224224- let verification_recipient = match verification_channel {
225225- "email" => match &input.email {
226226- Some(email) if !email.trim().is_empty() => email.trim().to_string(),
227227- _ => return (
228228- StatusCode::BAD_REQUEST,
229229- Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})),
230230- ).into_response(),
231231- },
232232- "discord" => match &input.discord_id {
233233- Some(id) if !id.trim().is_empty() => id.trim().to_string(),
234234- _ => return (
235235- StatusCode::BAD_REQUEST,
236236- Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})),
237237- ).into_response(),
238238- },
239239- "telegram" => match &input.telegram_username {
240240- Some(username) if !username.trim().is_empty() => username.trim().to_string(),
241241- _ => return (
242242- StatusCode::BAD_REQUEST,
243243- Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})),
244244- ).into_response(),
245245- },
246246- "signal" => match &input.signal_number {
247247- Some(number) if !number.trim().is_empty() => number.trim().to_string(),
248248- _ => return (
249249- StatusCode::BAD_REQUEST,
250250- Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})),
251251- ).into_response(),
252252- },
253253- _ => return (
254254- StatusCode::BAD_REQUEST,
255255- Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})),
256256- ).into_response(),
257257- };
258258-259390 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000);
260391 let code_expires_at = chrono::Utc::now() + chrono::Duration::minutes(30);
261392···325456 }
326457 };
327458328328- let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
329329- if let Some(signing_key_did) = &input.signing_key {
330330- let reserved = sqlx::query!(
331331- r#"
332332- SELECT id, private_key_bytes
333333- FROM reserved_signing_keys
334334- WHERE public_key_did_key = $1
335335- AND used_at IS NULL
336336- AND expires_at > NOW()
337337- FOR UPDATE
338338- "#,
339339- signing_key_did
340340- )
341341- .fetch_optional(&mut *tx)
342342- .await;
343343-344344- match reserved {
345345- Ok(Some(row)) => (row.private_key_bytes, Some(row.id)),
346346- Ok(None) => {
347347- return (
348348- StatusCode::BAD_REQUEST,
349349- Json(json!({
350350- "error": "InvalidSigningKey",
351351- "message": "Signing key not found, already used, or expired"
352352- })),
353353- )
354354- .into_response();
355355- }
356356- Err(e) => {
357357- error!("Error looking up reserved signing key: {:?}", e);
358358- return (
359359- StatusCode::INTERNAL_SERVER_ERROR,
360360- Json(json!({"error": "InternalError"})),
361361- )
362362- .into_response();
363363- }
364364- }
365365- } else {
366366- let secret_key = SecretKey::random(&mut OsRng);
367367- (secret_key.to_bytes().to_vec(), None)
368368- };
369369-370459 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
371460 Ok(enc) => enc,
372461 Err(e) => {
···442531 let rev = Tid::now(LimitedU32::MIN);
443532444533 let unsigned_commit = Commit::new_unsigned(did_obj, mst_root, rev, None);
445445-446446- let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
447447- Ok(k) => k,
448448- Err(e) => {
449449- error!("Error creating signing key: {:?}", e);
450450- return (
451451- StatusCode::INTERNAL_SERVER_ERROR,
452452- Json(json!({"error": "InternalError"})),
453453- )
454454- .into_response();
455455- }
456456- };
457534458535 let signed_commit = match unsigned_commit.sign(&signing_key) {
459536 Ok(c) => c,
+44-2
src/api/identity/did.rs
···33use axum::{
44 Json,
55 extract::{Path, Query, State},
66- http::StatusCode,
66+ http::{HeaderMap, StatusCode},
77 response::{IntoResponse, Response},
88};
99use base64::Engine;
···3838 return (StatusCode::OK, Json(json!({ "did": did }))).into_response();
3939 }
40404141- let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", handle)
4141+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
4242+ let suffix = format!(".{}", hostname);
4343+ let short_handle = if handle.ends_with(&suffix) {
4444+ handle.strip_suffix(&suffix).unwrap_or(handle)
4545+ } else {
4646+ handle
4747+ };
4848+4949+ let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle)
4250 .fetch_optional(&state.db)
4351 .await;
4452···452460 }
453461 }
454462}
463463+464464+pub async fn well_known_atproto_did(
465465+ State(state): State<AppState>,
466466+ headers: HeaderMap,
467467+) -> Response {
468468+ let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
469469+ Some(h) => h,
470470+ None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
471471+ };
472472+473473+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
474474+ let suffix = format!(".{}", hostname);
475475+476476+ let handle = host.split(':').next().unwrap_or(host);
477477+478478+ let short_handle = if handle.ends_with(&suffix) {
479479+ handle.strip_suffix(&suffix).unwrap_or(handle)
480480+ } else {
481481+ return (StatusCode::NOT_FOUND, "Handle not found").into_response();
482482+ };
483483+484484+ let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle)
485485+ .fetch_optional(&state.db)
486486+ .await;
487487+488488+ match user {
489489+ Ok(Some(row)) => row.did.into_response(),
490490+ Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
491491+ Err(e) => {
492492+ error!("DB error in well-known atproto-did: {:?}", e);
493493+ (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
494494+ }
495495+ }
496496+}
+1
src/api/identity/mod.rs
···55pub use account::create_account;
66pub use did::{
77 get_recommended_did_credentials, resolve_handle, update_handle, user_did_doc, well_known_did,
88+ well_known_atproto_did,
89};
910pub use plc::{request_plc_operation_signature, sign_plc_operation, submit_plc_operation};
+12-3
src/api/repo/meta.rs
···1717 State(state): State<AppState>,
1818 Query(input): Query<DescribeRepoInput>,
1919) -> Response {
2020+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2121+2022 let user_row = if input.repo.starts_with("did:") {
2123 sqlx::query!("SELECT id, handle, did FROM users WHERE did = $1", input.repo)
2224 .fetch_optional(&state.db)
2325 .await
2426 .map(|opt| opt.map(|r| (r.id, r.handle, r.did)))
2527 } else {
2626- sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", input.repo)
2828+ let suffix = format!(".{}", hostname);
2929+ let short_handle = if input.repo.ends_with(&suffix) {
3030+ input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
3131+ } else {
3232+ &input.repo
3333+ };
3434+ sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", short_handle)
2735 .fetch_optional(&state.db)
2836 .await
2937 .map(|opt| opt.map(|r| (r.id, r.handle, r.did)))
···5058 Err(_) => Vec::new(),
5159 };
52606161+ let full_handle = format!("{}.{}", handle, hostname);
5362 let did_doc = json!({
5463 "id": did,
5555- "alsoKnownAs": [format!("at://{}", handle)]
6464+ "alsoKnownAs": [format!("at://{}", full_handle)]
5665 });
57665867 Json(json!({
5959- "handle": handle,
6868+ "handle": full_handle,
6069 "did": did,
6170 "didDoc": did_doc,
6271 "collections": collections,
+18-2
src/api/repo/record/read.rs
···2525 State(state): State<AppState>,
2626 Query(input): Query<GetRecordInput>,
2727) -> Response {
2828+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
2929+2830 let user_id_opt = if input.repo.starts_with("did:") {
2931 sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
3032 .fetch_optional(&state.db)
3133 .await
3234 .map(|opt| opt.map(|r| r.id))
3335 } else {
3434- sqlx::query!("SELECT id FROM users WHERE handle = $1", input.repo)
3636+ let suffix = format!(".{}", hostname);
3737+ let short_handle = if input.repo.ends_with(&suffix) {
3838+ input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
3939+ } else {
4040+ &input.repo
4141+ };
4242+ sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle)
3543 .fetch_optional(&state.db)
3644 .await
3745 .map(|opt| opt.map(|r| r.id))
···143151 State(state): State<AppState>,
144152 Query(input): Query<ListRecordsInput>,
145153) -> Response {
154154+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
155155+146156 let user_id_opt = if input.repo.starts_with("did:") {
147157 sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
148158 .fetch_optional(&state.db)
149159 .await
150160 .map(|opt| opt.map(|r| r.id))
151161 } else {
152152- sqlx::query!("SELECT id FROM users WHERE handle = $1", input.repo)
162162+ let suffix = format!(".{}", hostname);
163163+ let short_handle = if input.repo.ends_with(&suffix) {
164164+ input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
165165+ } else {
166166+ &input.repo
167167+ };
168168+ sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle)
153169 .fetch_optional(&state.db)
154170 .await
155171 .map(|opt| opt.map(|r| r.id))