diff --git a/Cargo.lock b/Cargo.lock index 2f18585..8e48041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6092,9 +6092,57 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tranquil-api" +version = "0.4.2" +dependencies = [ + "anyhow", + "axum", + "backon", + "base32", + "base64 0.22.1", + "bcrypt", + "bs58", + "bytes", + "chrono", + "cid", + "ed25519-dalek", + "futures", + "hex", + "http 1.4.0", + "infer", + "ipld-core", + "jacquard-common", + "jacquard-repo", + "k256", + "multibase", + "multihash", + "rand 0.8.5", + "reqwest", + "serde", + "serde_ipld_dagcbor", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.17", + "tokio", + "tracing", + "tranquil-config", + "tranquil-db", + "tranquil-db-traits", + "tranquil-lexicon", + "tranquil-pds", + "tranquil-scopes", + "tranquil-types", + "urlencoding", + "uuid", + "webauthn-rs", + "zip", +] + [[package]] name = "tranquil-auth" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "base32", @@ -6117,7 +6165,7 @@ dependencies = [ [[package]] name = "tranquil-cache" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "base64 0.22.1", @@ -6131,7 +6179,7 @@ dependencies = [ [[package]] name = "tranquil-comms" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "base64 0.22.1", @@ -6146,7 +6194,7 @@ dependencies = [ [[package]] name = "tranquil-config" -version = "0.4.1" +version = "0.4.2" dependencies = [ "confique", "serde", @@ -6154,7 +6202,7 @@ dependencies = [ [[package]] name = "tranquil-crypto" -version = "0.4.1" +version = "0.4.2" dependencies = [ "aes-gcm", "base64 0.22.1", @@ -6170,7 +6218,7 @@ dependencies = [ [[package]] name = "tranquil-db" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "chrono", @@ -6187,7 +6235,7 @@ dependencies = [ [[package]] name = "tranquil-db-traits" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "base64 0.22.1", @@ -6203,7 +6251,7 @@ dependencies = [ [[package]] name = "tranquil-infra" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "bytes", @@ -6214,7 +6262,7 @@ dependencies = [ [[package]] name = "tranquil-lexicon" -version = "0.4.1" +version = "0.4.2" dependencies = [ "chrono", "hickory-resolver", @@ -6232,7 +6280,7 @@ dependencies = [ [[package]] name = "tranquil-oauth" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "axum", @@ -6253,9 +6301,42 @@ dependencies = [ "uuid", ] +[[package]] +name = "tranquil-oauth-server" +version = "0.4.2" +dependencies = [ + "axum", + "base64 0.22.1", + "bcrypt", + "chrono", + "cid", + "hmac", + "http 1.4.0", + "jacquard-common", + "jacquard-repo", + "k256", + "rand 0.8.5", + "serde", + "serde_json", + "serde_urlencoded", + "sha2", + "subtle", + "tokio", + "tracing", + "tranquil-api", + "tranquil-config", + "tranquil-crypto", + "tranquil-db-traits", + "tranquil-pds", + "tranquil-types", + "urlencoding", + "uuid", + "webauthn-rs", +] + [[package]] name = "tranquil-pds" -version = "0.4.1" +version = "0.4.2" dependencies = [ "aes-gcm", "anyhow", @@ -6272,9 +6353,7 @@ dependencies = [ "chrono", "ciborium", "cid", - "clap", "ctor", - "dotenvy", "ed25519-dalek", "futures", "futures-util", @@ -6319,7 +6398,7 @@ dependencies = [ "tower-http", "tower-layer", "tracing", - "tracing-subscriber", + "tranquil-api", "tranquil-auth", "tranquil-cache", "tranquil-comms", @@ -6329,10 +6408,12 @@ dependencies = [ "tranquil-db-traits", "tranquil-lexicon", "tranquil-oauth", + "tranquil-oauth-server", "tranquil-repo", "tranquil-ripple", "tranquil-scopes", "tranquil-storage", + "tranquil-sync", "tranquil-types", "urlencoding", "uuid", @@ -6343,7 +6424,7 @@ dependencies = [ [[package]] name = "tranquil-repo" -version = "0.4.1" +version = "0.4.2" dependencies = [ "bytes", "cid", @@ -6355,7 +6436,7 @@ dependencies = [ [[package]] name = "tranquil-ripple" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "backon", @@ -6380,7 +6461,7 @@ dependencies = [ [[package]] name = "tranquil-scopes" -version = "0.4.1" +version = "0.4.2" dependencies = [ "axum", "futures", @@ -6394,9 +6475,29 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "tranquil-server" +version = "0.4.2" +dependencies = [ + "axum", + "clap", + "dotenvy", + "ed25519-dalek", + "hex", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "tranquil-api", + "tranquil-config", + "tranquil-oauth-server", + "tranquil-pds", + "tranquil-sync", +] + [[package]] name = "tranquil-storage" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "aws-config", @@ -6411,9 +6512,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "tranquil-sync" +version = "0.4.2" +dependencies = [ + "anyhow", + "axum", + "bytes", + "chrono", + "cid", + "futures", + "ipld-core", + "jacquard-repo", + "serde", + "serde_ipld_dagcbor", + "tokio", + "tracing", + "tranquil-config", + "tranquil-db-traits", + "tranquil-pds", + "tranquil-types", +] + [[package]] name = "tranquil-types" -version = "0.4.1" +version = "0.4.2" dependencies = [ "chrono", "cid", diff --git a/Cargo.toml b/Cargo.toml index c8ca6d7..22f8145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,15 @@ members = [ "crates/tranquil-db-traits", "crates/tranquil-db", "crates/tranquil-pds", + "crates/tranquil-server", + "crates/tranquil-sync", + "crates/tranquil-oauth-server", + "crates/tranquil-api", "crates/tranquil-lexicon", ] [workspace.package] -version = "0.4.1" +version = "0.4.2" edition = "2024" license = "AGPL-3.0-or-later" @@ -40,6 +44,11 @@ tranquil-db-traits = { path = "crates/tranquil-db-traits" } tranquil-db = { path = "crates/tranquil-db" } tranquil-ripple = { path = "crates/tranquil-ripple" } tranquil-lexicon = { path = "crates/tranquil-lexicon" } +tranquil-pds = { path = "crates/tranquil-pds" } +tranquil-server = { path = "crates/tranquil-server" } +tranquil-sync = { path = "crates/tranquil-sync" } +tranquil-oauth-server = { path = "crates/tranquil-oauth-server" } +tranquil-api = { path = "crates/tranquil-api" } unicode-segmentation = "1" diff --git a/Dockerfile b/Dockerfile index 8d9fd2e..34ea471 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,15 +25,19 @@ COPY crates/tranquil-ripple ./crates/tranquil-ripple COPY crates/tranquil-storage ./crates/tranquil-storage COPY crates/tranquil-cache ./crates/tranquil-cache COPY crates/tranquil-pds ./crates/tranquil-pds +COPY crates/tranquil-sync ./crates/tranquil-sync +COPY crates/tranquil-api ./crates/tranquil-api +COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server +COPY crates/tranquil-server ./crates/tranquil-server COPY migrations ./crates/tranquil-pds/migrations RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/app/target \ if [ "$SLIM" = "true" ]; then \ - SQLX_OFFLINE=true cargo build --release -p tranquil-pds --no-default-features; \ + SQLX_OFFLINE=true cargo build --release -p tranquil-server --no-default-features; \ else \ - SQLX_OFFLINE=true cargo build --release -p tranquil-pds; \ + SQLX_OFFLINE=true cargo build --release -p tranquil-server; \ fi && \ - cp target/release/tranquil-pds /tmp/tranquil-pds + cp target/release/tranquil-server /tmp/tranquil-pds FROM alpine:3.23 AS signal-cli RUN apk add --no-cache curl tar diff --git a/crates/tranquil-api/Cargo.toml b/crates/tranquil-api/Cargo.toml new file mode 100644 index 0000000..1dfc6bf --- /dev/null +++ b/crates/tranquil-api/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "tranquil-api" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-pds = { workspace = true } +tranquil-types = { workspace = true } +tranquil-config = { workspace = true } +tranquil-db = { workspace = true } +tranquil-db-traits = { workspace = true } +tranquil-lexicon = { workspace = true, features = ["resolve"] } +tranquil-scopes = { workspace = true } + +anyhow = { workspace = true } +axum = { workspace = true } +backon = { workspace = true } +base32 = { workspace = true } +base64 = { workspace = true } +bcrypt = { workspace = true } +bs58 = { workspace = true } +bytes = { workspace = true } +chrono = { workspace = true } +cid = { workspace = true } +ed25519-dalek = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +http = { workspace = true } +infer = { workspace = true } +ipld-core = { workspace = true } +jacquard-common = { workspace = true } +jacquard-repo = { workspace = true } +k256 = { workspace = true } +multibase = { workspace = true } +multihash = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_ipld_dagcbor = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } +webauthn-rs = { workspace = true } +zip = { workspace = true } diff --git a/crates/tranquil-pds/src/api/actor/mod.rs b/crates/tranquil-api/src/actor/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/actor/mod.rs rename to crates/tranquil-api/src/actor/mod.rs diff --git a/crates/tranquil-pds/src/api/actor/preferences.rs b/crates/tranquil-api/src/actor/preferences.rs similarity index 98% rename from crates/tranquil-pds/src/api/actor/preferences.rs rename to crates/tranquil-api/src/actor/preferences.rs index ba8f102..8cc7fdc 100644 --- a/crates/tranquil-pds/src/api/actor/preferences.rs +++ b/crates/tranquil-api/src/actor/preferences.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::auth::{Auth, NotTakendown, Permissive}; -use crate::state::AppState; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Auth, NotTakendown, Permissive}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, diff --git a/crates/tranquil-pds/src/api/admin/account/delete.rs b/crates/tranquil-api/src/admin/account/delete.rs similarity index 76% rename from crates/tranquil-pds/src/api/admin/account/delete.rs rename to crates/tranquil-api/src/admin/account/delete.rs index 75a48a4..e0c128c 100644 --- a/crates/tranquil-pds/src/api/admin/account/delete.rs +++ b/crates/tranquil-api/src/admin/account/delete.rs @@ -1,8 +1,8 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::Did; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::Did; use axum::{ Json, extract::State, @@ -36,7 +36,7 @@ pub async fn delete_account( .await .log_db_err("deleting account")?; - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, did, tranquil_db_traits::AccountStatus::Deleted, @@ -50,7 +50,7 @@ pub async fn delete_account( } let _ = state .cache - .delete(&crate::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; Ok(EmptyResponse::ok().into_response()) } diff --git a/crates/tranquil-pds/src/api/admin/account/email.rs b/crates/tranquil-api/src/admin/account/email.rs similarity index 92% rename from crates/tranquil-pds/src/api/admin/account/email.rs rename to crates/tranquil-api/src/admin/account/email.rs index dc051a5..afa16f9 100644 --- a/crates/tranquil-pds/src/api/admin/account/email.rs +++ b/crates/tranquil-api/src/admin/account/email.rs @@ -1,7 +1,7 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::Did; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::Did; use axum::{ Json, extract::State, diff --git a/crates/tranquil-pds/src/api/admin/account/info.rs b/crates/tranquil-api/src/admin/account/info.rs similarity index 97% rename from crates/tranquil-pds/src/api/admin/account/info.rs rename to crates/tranquil-api/src/admin/account/info.rs index 9713618..83cb6b4 100644 --- a/crates/tranquil-pds/src/api/admin/account/info.rs +++ b/crates/tranquil-api/src/admin/account/info.rs @@ -1,7 +1,7 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::{Did, Handle}; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle}; use axum::{ Json, extract::{Query, RawQuery, State}, @@ -196,7 +196,7 @@ pub async fn get_account_infos( _auth: Auth, RawQuery(raw_query): RawQuery, ) -> Result { - let dids: Vec = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") + let dids: Vec = tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids") .into_iter() .filter(|d| !d.is_empty()) .collect(); diff --git a/crates/tranquil-pds/src/api/admin/account/mod.rs b/crates/tranquil-api/src/admin/account/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/admin/account/mod.rs rename to crates/tranquil-api/src/admin/account/mod.rs diff --git a/crates/tranquil-pds/src/api/admin/account/search.rs b/crates/tranquil-api/src/admin/account/search.rs similarity index 94% rename from crates/tranquil-pds/src/api/admin/account/search.rs rename to crates/tranquil-api/src/admin/account/search.rs index 1b2ed65..554944d 100644 --- a/crates/tranquil-pds/src/api/admin/account/search.rs +++ b/crates/tranquil-api/src/admin/account/search.rs @@ -1,7 +1,7 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::{Did, Handle}; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle}; use axum::{ Json, extract::{Query, State}, diff --git a/crates/tranquil-pds/src/api/admin/account/update.rs b/crates/tranquil-api/src/admin/account/update.rs similarity index 89% rename from crates/tranquil-pds/src/api/admin/account/update.rs rename to crates/tranquil-api/src/admin/account/update.rs index 9661440..411c823 100644 --- a/crates/tranquil-pds/src/api/admin/account/update.rs +++ b/crates/tranquil-api/src/admin/account/update.rs @@ -1,8 +1,8 @@ -use crate::api::EmptyResponse; -use crate::api::error::ApiError; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::{Did, Handle, PlainPassword}; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle, PlainPassword}; use axum::{ Json, extract::State, @@ -101,14 +101,14 @@ pub async fn update_account_handle( if let Some(old) = old_handle { let _ = state .cache - .delete(&crate::cache_keys::handle_key(&old)) + .delete(&tranquil_pds::cache_keys::handle_key(&old)) .await; } let _ = state .cache - .delete(&crate::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; - if let Err(e) = crate::api::repo::record::sequence_identity_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event( &state, did, Some(&handle_for_check), @@ -121,7 +121,7 @@ pub async fn update_account_handle( ); } if let Err(e) = - crate::api::identity::did::update_plc_handle(&state, did, &handle_for_check).await + crate::identity::did::update_plc_handle(&state, did, &handle_for_check).await { warn!("Failed to update PLC handle for admin handle update: {}", e); } diff --git a/crates/tranquil-pds/src/api/admin/config.rs b/crates/tranquil-api/src/admin/config.rs similarity index 98% rename from crates/tranquil-pds/src/api/admin/config.rs rename to crates/tranquil-api/src/admin/config.rs index 7f591ea..c27fff1 100644 --- a/crates/tranquil-pds/src/api/admin/config.rs +++ b/crates/tranquil-api/src/admin/config.rs @@ -1,6 +1,6 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use tracing::{error, warn}; diff --git a/crates/tranquil-pds/src/api/admin/invite.rs b/crates/tranquil-api/src/admin/invite.rs similarity index 97% rename from crates/tranquil-pds/src/api/admin/invite.rs rename to crates/tranquil-api/src/admin/invite.rs index b3f3083..4faa019 100644 --- a/crates/tranquil-pds/src/api/admin/invite.rs +++ b/crates/tranquil-api/src/admin/invite.rs @@ -1,7 +1,7 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::{Query, State}, diff --git a/crates/tranquil-pds/src/api/admin/mod.rs b/crates/tranquil-api/src/admin/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/admin/mod.rs rename to crates/tranquil-api/src/admin/mod.rs diff --git a/crates/tranquil-pds/src/api/admin/server_stats.rs b/crates/tranquil-api/src/admin/server_stats.rs similarity index 88% rename from crates/tranquil-pds/src/api/admin/server_stats.rs rename to crates/tranquil-api/src/admin/server_stats.rs index aab42a7..7004290 100644 --- a/crates/tranquil-pds/src/api/admin/server_stats.rs +++ b/crates/tranquil-api/src/admin/server_stats.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, diff --git a/crates/tranquil-pds/src/api/admin/status.rs b/crates/tranquil-api/src/admin/status.rs similarity index 96% rename from crates/tranquil-pds/src/api/admin/status.rs rename to crates/tranquil-api/src/admin/status.rs index d61e331..cb516c2 100644 --- a/crates/tranquil-pds/src/api/admin/status.rs +++ b/crates/tranquil-api/src/admin/status.rs @@ -1,7 +1,7 @@ -use crate::api::error::ApiError; -use crate::auth::{Admin, Auth}; -use crate::state::AppState; -use crate::types::{CidLink, Did}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Admin, Auth}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{CidLink, Did}; use axum::{ Json, extract::{Query, State}, @@ -215,7 +215,7 @@ pub async fn update_subject_status( tranquil_db_traits::AccountStatus::Active }; if let Err(e) = - crate::api::repo::record::sequence_account_event(&state, &did, status).await + tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await { warn!("Failed to sequence account event for takedown: {}", e); } @@ -227,7 +227,7 @@ pub async fn update_subject_status( tranquil_db_traits::AccountStatus::Active }; if let Err(e) = - crate::api::repo::record::sequence_account_event(&state, &did, status).await + tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await { warn!("Failed to sequence account event for deactivation: {}", e); } @@ -235,7 +235,7 @@ pub async fn update_subject_status( if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { let _ = state .cache - .delete(&crate::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; } return Ok(( diff --git a/crates/tranquil-pds/src/api/age_assurance.rs b/crates/tranquil-api/src/age_assurance.rs similarity index 86% rename from crates/tranquil-pds/src/api/age_assurance.rs rename to crates/tranquil-api/src/age_assurance.rs index 18946d5..d3e4b6d 100644 --- a/crates/tranquil-pds/src/api/age_assurance.rs +++ b/crates/tranquil-api/src/age_assurance.rs @@ -1,5 +1,5 @@ -use crate::auth::{AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop}; -use crate::state::AppState; +use tranquil_pds::auth::{AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -33,13 +33,13 @@ pub async fn get_age_assurance_state() -> Response { } async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option { - let auth_header = crate::util::get_header_str(headers, http::header::AUTHORIZATION); + let auth_header = tranquil_pds::util::get_header_str(headers, http::header::AUTHORIZATION); tracing::debug!(?auth_header, "age assurance: extracting token"); let extracted = extract_auth_token_from_header(auth_header)?; tracing::debug!("age assurance: got token, validating"); - let dpop_proof = crate::util::get_header_str(headers, crate::util::HEADER_DPOP); + let dpop_proof = tranquil_pds::util::get_header_str(headers, tranquil_pds::util::HEADER_DPOP); let http_uri = "/"; let auth_user = match validate_token_with_dpop( diff --git a/crates/tranquil-pds/src/api/backup.rs b/crates/tranquil-api/src/backup.rs similarity index 96% rename from crates/tranquil-pds/src/api/backup.rs rename to crates/tranquil-api/src/backup.rs index 32df352..fd7fbd3 100644 --- a/crates/tranquil-pds/src/api/backup.rs +++ b/crates/tranquil-api/src/backup.rs @@ -1,9 +1,9 @@ -use crate::api::error::ApiError; -use crate::api::{EmptyResponse, EnabledResponse}; -use crate::auth::{Active, Auth}; -use crate::scheduled::generate_full_backup; -use crate::state::AppState; -use crate::storage::{BackupStorage, backup_retention_count}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::api::{EmptyResponse, EnabledResponse}; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::scheduled::generate_full_backup; +use tranquil_pds::state::AppState; +use tranquil_pds::storage::{BackupStorage, backup_retention_count}; use anyhow::Context; use axum::{ Json, @@ -39,7 +39,7 @@ pub struct ListBackupsOutput { pub async fn list_backups( State(state): State, auth: Auth, -) -> Result { +) -> Result { let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await { Ok(Some(status)) => status, @@ -91,7 +91,7 @@ pub async fn get_backup( State(state): State, auth: Auth, Query(query): Query, -) -> Result { +) -> Result { let backup_id = match uuid::Uuid::parse_str(&query.id) { Ok(id) => id, Err(_) => { @@ -157,7 +157,7 @@ pub struct CreateBackupOutput { pub async fn create_backup( State(state): State, auth: Auth, -) -> Result { +) -> Result { let backup_storage = match state.backup_storage.as_ref() { Some(storage) => storage, None => { @@ -213,7 +213,7 @@ pub async fn create_backup( } }; - let block_count = crate::scheduled::count_car_blocks(&car_bytes); + let block_count = tranquil_pds::scheduled::count_car_blocks(&car_bytes); let size_bytes = i64::try_from(car_bytes.len()).unwrap_or(i64::MAX); let storage_key = match backup_storage @@ -327,7 +327,7 @@ pub async fn delete_backup( State(state): State, auth: Auth, Query(query): Query, -) -> Result { +) -> Result { let backup_id = match uuid::Uuid::parse_str(&query.id) { Ok(id) => id, Err(_) => { @@ -384,7 +384,7 @@ pub async fn set_backup_enabled( State(state): State, auth: Auth, Json(input): Json, -) -> Result { +) -> Result { let deactivated_at = match state .backup_repo .get_user_deactivated_status(&auth.did) @@ -423,7 +423,7 @@ pub async fn set_backup_enabled( pub async fn export_blobs( State(state): State, auth: Auth, -) -> Result { +) -> Result { let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await { Ok(Some(id)) => id, Ok(None) => { diff --git a/crates/tranquil-pds/src/api/delegation.rs b/crates/tranquil-api/src/delegation.rs similarity index 94% rename from crates/tranquil-pds/src/api/delegation.rs rename to crates/tranquil-api/src/delegation.rs index ca669b2..77e1e92 100644 --- a/crates/tranquil-pds/src/api/delegation.rs +++ b/crates/tranquil-api/src/delegation.rs @@ -1,13 +1,13 @@ -use crate::api::error::ApiError; -use crate::api::repo::record::utils::create_signed_commit; -use crate::auth::{Active, Auth}; -use crate::delegation::{ +use tranquil_pds::api::error::ApiError; +use tranquil_pds::repo_ops::create_signed_commit; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::delegation::{ DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers, verify_can_be_controller, verify_can_control_accounts, }; -use crate::rate_limit::{AccountCreationLimit, RateLimited}; -use crate::state::AppState; -use crate::types::{Did, Handle}; +use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle}; use axum::{ Json, extract::{Query, State}, @@ -449,7 +449,7 @@ pub async fn create_delegated_account( .unwrap_or(&input.handle), None => &input.handle, }; - match crate::api::validation::validate_short_handle(handle_to_validate) { + match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])), Err(e) => { return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); @@ -465,7 +465,7 @@ pub async fn create_delegated_account( .map(|e| e.trim().to_string()) .filter(|e| !e.is_empty()); if let Some(ref email) = email - && !crate::api::validation::is_valid_email(email) + && !tranquil_pds::api::validation::is_valid_email(email) { return Ok(ApiError::InvalidEmail.into_response()); } @@ -502,9 +502,9 @@ pub async fn create_delegated_account( .secrets .plc_rotation_key .clone() - .unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key)); + .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key)); - let genesis_result = match crate::plc::create_genesis_operation( + let genesis_result = match tranquil_pds::plc::create_genesis_operation( &signing_key, &rotation_key, &handle, @@ -520,7 +520,7 @@ pub async fn create_delegated_account( } }; - let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone())); + let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone())); if let Err(e) = plc_client .send_operation(&genesis_result.did, &genesis_result.signed_operation) .await @@ -540,7 +540,7 @@ pub async fn create_delegated_account( let handle: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?; info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account"); - let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { + let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) { Ok(bytes) => bytes, Err(e) => { error!("Error encrypting signing key: {:?}", e); @@ -581,7 +581,7 @@ pub async fn create_delegated_account( controller_did: can_control.did().clone(), controller_scopes: input.controller_scopes.as_str().to_string(), encrypted_key_bytes, - encryption_version: crate::config::ENCRYPTION_VERSION, + encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, commit_cid: commit_cid.to_string(), repo_rev: rev.as_ref().to_string(), genesis_block_cids, @@ -616,11 +616,11 @@ pub async fn create_delegated_account( } if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await { warn!("Failed to sequence identity event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did, tranquil_db_traits::AccountStatus::Active, @@ -634,11 +634,11 @@ pub async fn create_delegated_account( "$type": "app.bsky.actor.profile", "displayName": handle }); - if let Err(e) = crate::api::repo::record::create_record_internal( + if let Err(e) = tranquil_pds::repo_ops::create_record_internal( &state, &did, - &crate::types::PROFILE_COLLECTION, - &crate::types::PROFILE_RKEY, + &tranquil_pds::types::PROFILE_COLLECTION, + &tranquil_pds::types::PROFILE_RKEY, &profile_record, ) .await diff --git a/crates/tranquil-pds/src/api/discord_webhook.rs b/crates/tranquil-api/src/discord_webhook.rs similarity index 98% rename from crates/tranquil-pds/src/api/discord_webhook.rs rename to crates/tranquil-api/src/discord_webhook.rs index ecca1b2..63b08e2 100644 --- a/crates/tranquil-pds/src/api/discord_webhook.rs +++ b/crates/tranquil-api/src/discord_webhook.rs @@ -10,9 +10,9 @@ use serde_json::json; use tracing::{debug, info, warn}; use tranquil_types::Handle; -use crate::comms::comms_repo; -use crate::state::AppState; -use crate::util::discord_public_key; +use tranquil_pds::comms::comms_repo; +use tranquil_pds::state::AppState; +use tranquil_pds::util::discord_public_key; #[derive(Deserialize)] struct Interaction { diff --git a/crates/tranquil-pds/src/api/identity/account.rs b/crates/tranquil-api/src/identity/account.rs similarity index 90% rename from crates/tranquil-pds/src/api/identity/account.rs rename to crates/tranquil-api/src/identity/account.rs index d67a5bc..5043cb9 100644 --- a/crates/tranquil-pds/src/api/identity/account.rs +++ b/crates/tranquil-api/src/identity/account.rs @@ -1,12 +1,12 @@ use super::did::verify_did_web; -use crate::api::error::ApiError; -use crate::api::repo::record::utils::create_signed_commit; -use crate::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token}; -use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; -use crate::rate_limit::{AccountCreationLimit, RateLimited}; -use crate::state::AppState; -use crate::types::{Did, Handle, PlainPassword}; -use crate::validation::validate_password; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::repo_ops::create_signed_commit; +use tranquil_pds::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token}; +use tranquil_pds::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; +use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle, PlainPassword}; +use tranquil_pds::validation::validate_password; use axum::{ Json, extract::State, @@ -73,7 +73,7 @@ pub async fn create_account( } let migration_auth = if let Some(extracted) = extract_auth_token_from_header( - crate::util::get_header_str(&headers, http::header::AUTHORIZATION), + tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION), ) { let token = extracted.token; if is_service_token(&token) { @@ -155,14 +155,14 @@ pub async fn create_account( .unwrap_or(&input.handle), None => &input.handle, }; - match crate::api::validation::validate_short_handle(handle_to_validate) { + match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => h, Err(e) => { return ApiError::from(e).into_response(); } } } else { - match crate::api::validation::validate_full_domain_handle(&input.handle) { + match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { Ok(h) => h, Err(e) => return ApiError::from(e).into_response(), } @@ -173,7 +173,7 @@ pub async fn create_account( .map(|e| e.trim().to_string()) .filter(|e| !e.is_empty()); if let Some(ref email) = email - && !crate::api::validation::is_valid_email(email) + && !tranquil_pds::api::validation::is_valid_email(email) { return ApiError::InvalidEmail.into_response(); } @@ -191,7 +191,7 @@ pub async fn create_account( tranquil_db_traits::CommsChannel::Discord => match &input.discord_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().to_lowercase(); - if !crate::api::validation::is_valid_discord_username(&clean) { + if !tranquil_pds::api::validation::is_valid_discord_username(&clean) { return ApiError::InvalidRequest( "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(), ).into_response(); @@ -203,7 +203,7 @@ pub async fn create_account( tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().trim_start_matches('@'); - if !crate::api::validation::is_valid_telegram_username(clean) { + if !tranquil_pds::api::validation::is_valid_telegram_username(clean) { return ApiError::InvalidRequest( "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(), ).into_response(); @@ -257,7 +257,7 @@ pub async fn create_account( let did_type = input.did_type.as_deref().unwrap_or("plc"); let did = match did_type { "web" => { - if !crate::api::server::meta::is_self_hosted_did_web_enabled() { + if !tranquil_pds::util::is_self_hosted_did_web_enabled() { return ApiError::SelfHostedDidWebDisabled.into_response(); } let encoded_handle = handle.replace(':', "%3A"); @@ -408,7 +408,7 @@ pub async fn create_account( .await { Ok(Some(key_info)) => { - match crate::config::decrypt_key( + match tranquil_pds::config::decrypt_key( &key_info.key_bytes, key_info.encryption_version, ) { @@ -428,14 +428,14 @@ pub async fn create_account( } }; let access_meta = - match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { + match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { Ok(m) => m, Err(e) => { error!("Error creating access token: {:?}", e); return ApiError::InternalError(None).into_response(); } }; - let refresh_meta = match crate::auth::create_refresh_token_with_metadata( + let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata( &did, &secret_key_bytes, ) { @@ -463,12 +463,12 @@ pub async fn create_account( } let hostname = &tranquil_config::get().server.hostname; let verification_required = if let Some(ref user_email) = email { - let token = crate::auth::verification_token::generate_migration_token( + let token = tranquil_pds::auth::verification_token::generate_migration_token( &did_typed, user_email, ); let formatted_token = - crate::auth::verification_token::format_token_for_display(&token); - if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification( + tranquil_pds::auth::verification_token::format_token_for_display(&token); + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), reactivated.user_id, @@ -590,7 +590,7 @@ pub async fn create_account( None }; - let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { + let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) { Ok(enc) => enc, Err(e) => { error!("Error encrypting user key: {:?}", e); @@ -666,7 +666,7 @@ pub async fn create_account( .map(|s| s.to_lowercase()), deactivated_at, encrypted_key_bytes, - encryption_version: crate::config::ENCRYPTION_VERSION, + encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, reserved_key_id, commit_cid: commit_cid_str.clone(), repo_rev: rev_str.clone(), @@ -697,7 +697,7 @@ pub async fn create_account( }; let user_id = create_result.user_id; if !is_migration && !is_did_web_byod { - if let Err(e) = crate::api::repo::record::sequence_identity_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event( &state, &did_for_commit, Some(&handle_typed), @@ -706,7 +706,7 @@ pub async fn create_account( { warn!("Failed to sequence identity event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did_for_commit, tranquil_db_traits::AccountStatus::Active, @@ -715,7 +715,7 @@ pub async fn create_account( { warn!("Failed to sequence account event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_genesis_commit( + if let Err(e) = tranquil_pds::repo_ops::sequence_genesis_commit( &state, &did_for_commit, &commit_cid, @@ -726,7 +726,7 @@ pub async fn create_account( { warn!("Failed to sequence commit event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_sync_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event( &state, &did_for_commit, &commit_cid_str, @@ -740,11 +740,11 @@ pub async fn create_account( "$type": "app.bsky.actor.profile", "displayName": input.handle }); - if let Err(e) = crate::api::repo::record::create_record_internal( + if let Err(e) = tranquil_pds::repo_ops::create_record_internal( &state, &did_for_commit, - &crate::types::PROFILE_COLLECTION, - &crate::types::PROFILE_RKEY, + &tranquil_pds::types::PROFILE_COLLECTION, + &tranquil_pds::types::PROFILE_RKEY, &profile_record, ) .await @@ -755,14 +755,14 @@ pub async fn create_account( let hostname = &tranquil_config::get().server.hostname; if !is_migration { if let Some(ref recipient) = verification_recipient { - let verification_token = crate::auth::verification_token::generate_signup_token( + let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( &did_for_commit, verification_channel, recipient, ); let formatted_token = - crate::auth::verification_token::format_token_for_display(&verification_token); - if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( + tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -781,9 +781,9 @@ pub async fn create_account( } } else if let Some(ref user_email) = email { let token = - crate::auth::verification_token::generate_migration_token(&did_for_commit, user_email); - let formatted_token = crate::auth::verification_token::format_token_for_display(&token); - if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification( + tranquil_pds::auth::verification_token::generate_migration_token(&did_for_commit, user_email); + let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token); + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -797,7 +797,7 @@ pub async fn create_account( } } - let access_meta = match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) + let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { Ok(m) => m, Err(e) => { @@ -806,7 +806,7 @@ pub async fn create_account( } }; let refresh_meta = - match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) { + match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) { Ok(m) => m, Err(e) => { error!("createAccount: Error creating refresh token: {:?}", e); diff --git a/crates/tranquil-pds/src/api/identity/did.rs b/crates/tranquil-api/src/identity/did.rs similarity index 91% rename from crates/tranquil-pds/src/api/identity/did.rs rename to crates/tranquil-api/src/identity/did.rs index 45e1f72..032dc99 100644 --- a/crates/tranquil-pds/src/api/identity/did.rs +++ b/crates/tranquil-api/src/identity/did.rs @@ -1,12 +1,12 @@ -use crate::api::{ApiError, DidResponse, EmptyResponse}; -use crate::auth::{Auth, NotTakendown}; -use crate::plc::signing_key_to_did_key; -use crate::rate_limit::{ +use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse}; +use tranquil_pds::auth::{Auth, NotTakendown}; +use tranquil_pds::plc::signing_key_to_did_key; +use tranquil_pds::rate_limit::{ HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message, }; -use crate::state::AppState; -use crate::types::Handle; -use crate::util::get_header_str; +use tranquil_pds::state::AppState; +use tranquil_pds::types::Handle; +use tranquil_pds::util::get_header_str; use axum::{ Json, extract::{Path, Query, State}, @@ -42,7 +42,7 @@ pub async fn resolve_handle( if handle_str.is_empty() { return ApiError::InvalidRequest("handle is required".into()).into_response(); } - let cache_key = crate::cache_keys::handle_key(handle_str); + let cache_key = tranquil_pds::cache_keys::handle_key(handle_str); if let Some(did) = state.cache.get(&cache_key).await { return DidResponse::response(did).into_response(); } @@ -61,7 +61,7 @@ pub async fn resolve_handle( .await; DidResponse::response(row.did).into_response() } - Ok(None) => match crate::handle::resolve_handle(handle.as_str()).await { + Ok(None) => match tranquil_pds::handle::resolve_handle(handle.as_str()).await { Ok(did) => { let _ = state .cache @@ -148,7 +148,7 @@ pub async fn well_known_did(State(state): State, headers: HeaderMap) - "id": did, "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": format!("https://{}", hostname) }] })) @@ -158,7 +158,7 @@ pub async fn well_known_did(State(state): State, headers: HeaderMap) - async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response { let encoded_handle = handle.replace(':', "%3A"); let expected_did = format!("did:web:{}", encoded_handle); - let expected_did_typed: crate::types::Did = match expected_did.parse() { + let expected_did_typed: tranquil_pds::types::Did = match expected_did.parse() { Ok(d) => d, Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), }; @@ -216,7 +216,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> })).collect::>(), "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": service_endpoint }] })) @@ -229,7 +229,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Err(_) => return ApiError::InternalError(None).into_response(), }; let key_bytes: Vec = - match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { + match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { Ok(k) => k, Err(_) => { return ApiError::InternalError(None).into_response(); @@ -269,7 +269,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> }], "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": service_endpoint }] })) @@ -351,7 +351,7 @@ pub async fn user_did_doc(State(state): State, Path(handle): Path>(), "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": service_endpoint }] })) @@ -364,7 +364,7 @@ pub async fn user_did_doc(State(state): State, Path(handle): Path return ApiError::InternalError(None).into_response(), }; let key_bytes: Vec = - match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { + match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { Ok(k) => k, Err(_) => { return ApiError::InternalError(None).into_response(); @@ -404,7 +404,7 @@ pub async fn user_did_doc(State(state): State, Path(handle): Path, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_identity_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::IdentityAttr::Handle, + tranquil_pds::oauth::scopes::IdentityAttr::Handle, ) { return Ok(e); } @@ -672,7 +672,7 @@ pub async fn update_handle( "Handle segment cannot start or end with hyphen".into(), ))); } - if crate::moderation::has_explicit_slur(&new_handle) { + if tranquil_pds::moderation::has_explicit_slur(&new_handle) { return Err(ApiError::InvalidHandle(Some( "Inappropriate language in handle".into(), ))); @@ -704,7 +704,7 @@ pub async fn update_handle( Err(_) => return Err(ApiError::InvalidHandle(None)), }; if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)) .await { warn!("Failed to sequence identity event for handle update: {}", e); @@ -730,19 +730,19 @@ pub async fn update_handle( Err(_) => return Err(ApiError::InvalidHandle(None)), }; if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)) .await { warn!("Failed to sequence identity event for handle update: {}", e); } return Ok(EmptyResponse::ok().into_response()); } - match crate::handle::verify_handle_ownership(&new_handle, &did).await { + match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await { Ok(()) => {} - Err(crate::handle::HandleResolutionError::NotFound) => { + Err(tranquil_pds::handle::HandleResolutionError::NotFound) => { return Err(ApiError::HandleNotAvailable(None)); } - Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { + Err(tranquil_pds::handle::HandleResolutionError::DidMismatch { expected, actual }) => { return Err(ApiError::HandleNotAvailable(Some(format!( "Handle points to different DID. Expected {}, got {}", expected, actual @@ -781,15 +781,15 @@ pub async fn update_handle( if !current_handle.is_empty() { let _ = state .cache - .delete(&crate::cache_keys::handle_key(¤t_handle)) + .delete(&tranquil_pds::cache_keys::handle_key(¤t_handle)) .await; } let _ = state .cache - .delete(&crate::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await + tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)).await { warn!("Failed to sequence identity event for handle update: {}", e); } @@ -801,7 +801,7 @@ pub async fn update_handle( pub async fn update_plc_handle( state: &AppState, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, new_handle: &Handle, ) -> Result<(), Box> { if !did.as_str().starts_with("did:plc:") { @@ -811,20 +811,20 @@ pub async fn update_plc_handle( Some(r) => r, None => return Ok(()), }; - let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?; + let key_bytes = tranquil_pds::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?; let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?; - let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone())); + let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone())); let last_op = plc_client.get_last_op(did).await?; let new_also_known_as = vec![format!("at://{}", new_handle)]; let update_op = - crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?; - let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?; + tranquil_pds::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?; + let signed_op = tranquil_pds::plc::sign_operation(&update_op, &signing_key)?; plc_client.send_operation(did, &signed_op).await?; Ok(()) } pub async fn well_known_atproto_did(State(state): State, headers: HeaderMap) -> Response { - let host = match crate::util::get_header_str(&headers, http::header::HOST) { + let host = match tranquil_pds::util::get_header_str(&headers, http::header::HOST) { Some(h) => h, None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), }; diff --git a/crates/tranquil-pds/src/api/identity/handle.rs b/crates/tranquil-api/src/identity/handle.rs similarity index 87% rename from crates/tranquil-pds/src/api/identity/handle.rs rename to crates/tranquil-api/src/identity/handle.rs index 28f6be7..d2d5188 100644 --- a/crates/tranquil-pds/src/api/identity/handle.rs +++ b/crates/tranquil-api/src/identity/handle.rs @@ -1,5 +1,5 @@ -use crate::rate_limit::{HandleVerificationLimit, RateLimited}; -use crate::types::{Did, Handle}; +use tranquil_pds::rate_limit::{HandleVerificationLimit, RateLimited}; +use tranquil_pds::types::{Did, Handle}; use axum::{ Json, response::{IntoResponse, Response}, @@ -29,7 +29,7 @@ pub async fn verify_handle_ownership( let handle_str = input.handle.as_str(); let did_str = input.did.as_str(); - let dns_mismatch = match crate::handle::resolve_handle_dns(handle_str).await { + let dns_mismatch = match tranquil_pds::handle::resolve_handle_dns(handle_str).await { Ok(did) if did == did_str => { return Json(VerifyHandleOwnershipOutput { verified: true, @@ -45,7 +45,7 @@ pub async fn verify_handle_ownership( Err(_) => None, }; - match crate::handle::resolve_handle_http(handle_str).await { + match tranquil_pds::handle::resolve_handle_http(handle_str).await { Ok(did) if did == did_str => Json(VerifyHandleOwnershipOutput { verified: true, method: Some("http".to_string()), diff --git a/crates/tranquil-pds/src/api/identity/mod.rs b/crates/tranquil-api/src/identity/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/identity/mod.rs rename to crates/tranquil-api/src/identity/mod.rs diff --git a/crates/tranquil-pds/src/api/identity/plc/mod.rs b/crates/tranquil-api/src/identity/plc/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/identity/plc/mod.rs rename to crates/tranquil-api/src/identity/plc/mod.rs diff --git a/crates/tranquil-pds/src/api/identity/plc/request.rs b/crates/tranquil-api/src/identity/plc/request.rs similarity index 75% rename from crates/tranquil-pds/src/api/identity/plc/request.rs rename to crates/tranquil-api/src/identity/plc/request.rs index fe488c1..7192b71 100644 --- a/crates/tranquil-pds/src/api/identity/plc/request.rs +++ b/crates/tranquil-api/src/identity/plc/request.rs @@ -1,7 +1,7 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Auth, Permissive}; -use crate::state::AppState; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Auth, Permissive}; +use tranquil_pds::state::AppState; use axum::{ extract::State, response::{IntoResponse, Response}, @@ -10,17 +10,17 @@ use chrono::{Duration, Utc}; use tracing::{info, warn}; fn generate_plc_token() -> String { - crate::util::generate_token_code() + tranquil_pds::util::generate_token_code() } pub async fn request_plc_operation_signature( State(state): State, auth: Auth, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_identity_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::IdentityAttr::Wildcard, + tranquil_pds::oauth::scopes::IdentityAttr::Wildcard, ) { return Ok(e); } @@ -41,7 +41,7 @@ pub async fn request_plc_operation_signature( .log_db_err("creating PLC token")?; let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_plc_operation( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, diff --git a/crates/tranquil-pds/src/api/identity/plc/sign.rs b/crates/tranquil-api/src/identity/plc/sign.rs similarity index 88% rename from crates/tranquil-pds/src/api/identity/plc/sign.rs rename to crates/tranquil-api/src/identity/plc/sign.rs index ceaa82d..d98dfd5 100644 --- a/crates/tranquil-pds/src/api/identity/plc/sign.rs +++ b/crates/tranquil-api/src/identity/plc/sign.rs @@ -1,9 +1,9 @@ -use crate::api::ApiError; -use crate::api::error::DbResultExt; -use crate::auth::{Auth, Permissive}; -use crate::circuit_breaker::with_circuit_breaker; -use crate::plc::{PlcClient, PlcError, PlcService, ServiceType, create_update_op, sign_operation}; -use crate::state::AppState; +use tranquil_pds::api::ApiError; +use tranquil_pds::api::error::DbResultExt; +use tranquil_pds::auth::{Auth, Permissive}; +use tranquil_pds::circuit_breaker::with_circuit_breaker; +use tranquil_pds::plc::{PlcClient, PlcError, PlcService, ServiceType, create_update_op, sign_operation}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -44,10 +44,10 @@ pub async fn sign_plc_operation( auth: Auth, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_identity_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::IdentityAttr::Wildcard, + tranquil_pds::oauth::scopes::IdentityAttr::Wildcard, ) { return Ok(e); } @@ -86,7 +86,7 @@ pub async fn sign_plc_operation( .log_db_err("fetching user key")? .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; - let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + let key_bytes = tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) .map_err(|e| { error!("Failed to decrypt user key: {}", e); ApiError::InternalError(None) diff --git a/crates/tranquil-pds/src/api/identity/plc/submit.rs b/crates/tranquil-api/src/identity/plc/submit.rs similarity index 85% rename from crates/tranquil-pds/src/api/identity/plc/submit.rs rename to crates/tranquil-api/src/identity/plc/submit.rs index 7c12c2f..445c642 100644 --- a/crates/tranquil-pds/src/api/identity/plc/submit.rs +++ b/crates/tranquil-api/src/identity/plc/submit.rs @@ -1,9 +1,9 @@ -use crate::api::error::DbResultExt; -use crate::api::{ApiError, EmptyResponse}; -use crate::auth::{Auth, Permissive}; -use crate::circuit_breaker::with_circuit_breaker; -use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; -use crate::state::AppState; +use tranquil_pds::api::error::DbResultExt; +use tranquil_pds::api::{ApiError, EmptyResponse}; +use tranquil_pds::auth::{Auth, Permissive}; +use tranquil_pds::circuit_breaker::with_circuit_breaker; +use tranquil_pds::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -24,10 +24,10 @@ pub async fn submit_plc_operation( auth: Auth, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_identity_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::IdentityAttr::Wildcard, + tranquil_pds::oauth::scopes::IdentityAttr::Wildcard, ) { return Ok(e); } @@ -57,7 +57,7 @@ pub async fn submit_plc_operation( .log_db_err("fetching user key")? .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; - let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + let key_bytes = tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) .map_err(|e| { error!("Failed to decrypt user key: {}", e); ApiError::InternalError(None) @@ -89,7 +89,7 @@ pub async fn submit_plc_operation( { let service_type = pds.get("type").and_then(|v| v.as_str()); let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); - if service_type != Some(crate::plc::ServiceType::Pds.as_str()) { + if service_type != Some(tranquil_pds::plc::ServiceType::Pds.as_str()) { return Err(ApiError::InvalidRequest( "Incorrect type on atproto_pds service".into(), )); @@ -147,15 +147,15 @@ pub async fn submit_plc_operation( } let _ = state .cache - .delete(&crate::cache_keys::handle_key(&user.handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&user.handle)) .await; let _ = state .cache - .delete(&crate::cache_keys::plc_doc_key(did)) + .delete(&tranquil_pds::cache_keys::plc_doc_key(did)) .await; let _ = state .cache - .delete(&crate::cache_keys::plc_data_key(did)) + .delete(&tranquil_pds::cache_keys::plc_data_key(did)) .await; if state.did_resolver.refresh_did(did).await.is_none() { warn!(did = %did, "Failed to refresh DID cache after PLC update"); diff --git a/crates/tranquil-api/src/lib.rs b/crates/tranquil-api/src/lib.rs new file mode 100644 index 0000000..dc778e5 --- /dev/null +++ b/crates/tranquil-api/src/lib.rs @@ -0,0 +1,273 @@ +pub mod actor; +pub mod admin; +pub mod age_assurance; +pub mod backup; +pub mod delegation; +pub mod discord_webhook; +pub mod identity; +pub mod moderation; +pub mod notification_prefs; +pub mod repo; +pub mod server; +pub mod telegram_webhook; +pub mod temp; +pub mod verification; + +use tranquil_pds::state::AppState; + +pub fn api_routes() -> axum::Router { + use axum::routing::{get, post}; + + axum::Router::new() + .route("/_health", get(server::health)) + .route( + "/com.atproto.server.describeServer", + get(server::describe_server), + ) + .route( + "/com.atproto.server.createAccount", + post(identity::create_account), + ) + .route( + "/com.atproto.server.createSession", + post(server::create_session), + ) + .route( + "/com.atproto.server.getSession", + get(server::get_session), + ) + .route("/_account.listSessions", get(server::list_sessions)) + .route("/_account.revokeSession", post(server::revoke_session)) + .route( + "/_account.revokeAllSessions", + post(server::revoke_all_sessions), + ) + .route( + "/com.atproto.server.deleteSession", + post(server::delete_session), + ) + .route( + "/com.atproto.server.refreshSession", + post(server::refresh_session), + ) + .route( + "/com.atproto.server.confirmSignup", + post(server::confirm_signup), + ) + .route( + "/com.atproto.server.resendVerification", + post(server::resend_verification), + ) + .route( + "/com.atproto.server.getServiceAuth", + get(server::get_service_auth), + ) + .route( + "/com.atproto.identity.resolveHandle", + get(identity::resolve_handle), + ) + .route( + "/com.atproto.repo.createRecord", + post(repo::create_record), + ) + .route("/com.atproto.repo.putRecord", post(repo::put_record)) + .route("/com.atproto.repo.getRecord", get(repo::get_record)) + .route( + "/com.atproto.repo.deleteRecord", + post(repo::delete_record), + ) + .route( + "/com.atproto.repo.listRecords", + get(repo::list_records), + ) + .route( + "/com.atproto.repo.describeRepo", + get(repo::describe_repo), + ) + .route("/com.atproto.repo.uploadBlob", post(repo::upload_blob)) + .route( + "/com.atproto.repo.applyWrites", + post(repo::apply_writes), + ) + .route( + "/com.atproto.server.checkAccountStatus", + get(server::check_account_status), + ) + .route( + "/com.atproto.identity.getRecommendedDidCredentials", + get(identity::get_recommended_did_credentials), + ) + .route( + "/com.atproto.repo.listMissingBlobs", + get(repo::list_missing_blobs), + ) + .route( + "/com.atproto.moderation.createReport", + post(moderation::create_report), + ) + .route( + "/com.atproto.admin.getAccountInfo", + get(admin::get_account_info), + ) + .route( + "/com.atproto.admin.getAccountInfos", + get(admin::get_account_infos), + ) + .route( + "/com.atproto.admin.searchAccounts", + get(admin::search_accounts), + ) + .route( + "/com.atproto.server.activateAccount", + post(server::activate_account), + ) + .route( + "/com.atproto.server.deactivateAccount", + post(server::deactivate_account), + ) + .route( + "/com.atproto.server.requestAccountDelete", + post(server::request_account_delete), + ) + .route( + "/com.atproto.server.deleteAccount", + post(server::delete_account), + ) + .route( + "/com.atproto.server.requestPasswordReset", + post(server::request_password_reset), + ) + .route( + "/com.atproto.server.resetPassword", + post(server::reset_password), + ) + .route("/_account.changePassword", post(server::change_password)) + .route("/_account.removePassword", post(server::remove_password)) + .route("/_account.setPassword", post(server::set_password)) + .route("/_account.getPasswordStatus", get(server::get_password_status)) + .route("/_account.getReauthStatus", get(server::get_reauth_status)) + .route("/_account.reauthPassword", post(server::reauth_password)) + .route("/_account.reauthTotp", post(server::reauth_totp)) + .route("/_account.reauthPasskeyStart", post(server::reauth_passkey_start)) + .route("/_account.reauthPasskeyFinish", post(server::reauth_passkey_finish)) + .route("/_account.getLegacyLoginPreference", get(server::get_legacy_login_preference)) + .route("/_account.updateLegacyLoginPreference", post(server::update_legacy_login_preference)) + .route("/_account.updateLocale", post(server::update_locale)) + .route("/_account.listTrustedDevices", get(server::list_trusted_devices)) + .route("/_account.revokeTrustedDevice", post(server::revoke_trusted_device)) + .route("/_account.updateTrustedDevice", post(server::update_trusted_device)) + .route("/_account.createPasskeyAccount", post(server::create_passkey_account)) + .route("/_account.startPasskeyRegistrationForSetup", post(server::start_passkey_registration_for_setup)) + .route("/_account.completePasskeySetup", post(server::complete_passkey_setup)) + .route("/_account.requestPasskeyRecovery", post(server::request_passkey_recovery)) + .route("/_account.recoverPasskeyAccount", post(server::recover_passkey_account)) + .route("/_account.updateDidDocument", post(server::update_did_document)) + .route("/_account.getDidDocument", get(server::get_did_document)) + .route("/com.atproto.server.requestEmailUpdate", post(server::request_email_update)) + .route("/_checkEmailVerified", post(server::check_email_verified)) + .route("/_checkChannelVerified", post(server::check_channel_verified)) + .route("/com.atproto.server.confirmEmail", post(server::confirm_email)) + .route("/com.atproto.server.updateEmail", post(server::update_email)) + .route("/_account.authorizeEmailUpdate", get(server::authorize_email_update)) + .route("/_account.checkEmailUpdateStatus", get(server::check_email_update_status)) + .route("/_account.checkEmailInUse", post(server::check_email_in_use)) + .route("/_account.checkCommsChannelInUse", post(server::check_comms_channel_in_use)) + .route("/com.atproto.server.reserveSigningKey", post(server::reserve_signing_key)) + .route("/com.atproto.server.verifyMigrationEmail", post(server::verify_migration_email)) + .route("/com.atproto.server.resendMigrationVerification", post(server::resend_migration_verification)) + .route("/com.atproto.identity.updateHandle", post(identity::update_handle)) + .route("/com.atproto.identity.requestPlcOperationSignature", post(identity::request_plc_operation_signature)) + .route("/com.atproto.identity.signPlcOperation", post(identity::sign_plc_operation)) + .route("/com.atproto.identity.submitPlcOperation", post(identity::submit_plc_operation)) + .route("/_identity.verifyHandleOwnership", post(identity::verify_handle_ownership)) + .route("/com.atproto.repo.importRepo", post(repo::import_repo)) + .route("/com.atproto.admin.deleteAccount", post(admin::delete_account)) + .route("/com.atproto.admin.updateAccountEmail", post(admin::update_account_email)) + .route("/com.atproto.admin.updateAccountHandle", post(admin::update_account_handle)) + .route("/com.atproto.admin.updateAccountPassword", post(admin::update_account_password)) + .route("/com.atproto.server.listAppPasswords", get(server::list_app_passwords)) + .route("/com.atproto.server.createAppPassword", post(server::create_app_password)) + .route("/com.atproto.server.revokeAppPassword", post(server::revoke_app_password)) + .route("/com.atproto.server.createInviteCode", post(server::create_invite_code)) + .route("/com.atproto.server.createInviteCodes", post(server::create_invite_codes)) + .route("/com.atproto.server.getAccountInviteCodes", get(server::get_account_invite_codes)) + .route("/com.atproto.server.createTotpSecret", post(server::create_totp_secret)) + .route("/com.atproto.server.enableTotp", post(server::enable_totp)) + .route("/com.atproto.server.disableTotp", post(server::disable_totp)) + .route("/com.atproto.server.getTotpStatus", get(server::get_totp_status)) + .route("/com.atproto.server.regenerateBackupCodes", post(server::regenerate_backup_codes)) + .route("/com.atproto.server.startPasskeyRegistration", post(server::start_passkey_registration)) + .route("/com.atproto.server.finishPasskeyRegistration", post(server::finish_passkey_registration)) + .route("/com.atproto.server.listPasskeys", get(server::list_passkeys)) + .route("/com.atproto.server.deletePasskey", post(server::delete_passkey)) + .route("/com.atproto.server.updatePasskey", post(server::update_passkey)) + .route("/com.atproto.admin.getInviteCodes", get(admin::get_invite_codes)) + .route("/_admin.getServerStats", get(admin::get_server_stats)) + .route("/_server.getConfig", get(admin::get_server_config)) + .route("/_admin.updateServerConfig", post(admin::update_server_config)) + .route("/com.atproto.admin.disableAccountInvites", post(admin::disable_account_invites)) + .route("/com.atproto.admin.enableAccountInvites", post(admin::enable_account_invites)) + .route("/com.atproto.admin.disableInviteCodes", post(admin::disable_invite_codes)) + .route("/com.atproto.admin.getSubjectStatus", get(admin::get_subject_status)) + .route("/com.atproto.admin.updateSubjectStatus", post(admin::update_subject_status)) + .route("/com.atproto.admin.sendEmail", post(admin::send_email)) + .route("/app.bsky.actor.getPreferences", get(actor::get_preferences)) + .route("/app.bsky.actor.putPreferences", post(actor::put_preferences)) + .route("/com.atproto.temp.checkSignupQueue", get(temp::check_signup_queue)) + .route("/com.atproto.temp.dereferenceScope", post(temp::dereference_scope)) + .route("/_account.getNotificationPrefs", get(notification_prefs::get_notification_prefs)) + .route("/_account.updateNotificationPrefs", post(notification_prefs::update_notification_prefs)) + .route("/_account.getNotificationHistory", get(notification_prefs::get_notification_history)) + .route("/_account.confirmChannelVerification", post(verification::confirm_channel_verification)) + .route("/_account.verifyToken", post(server::verify_token)) + .route("/_delegation.listControllers", get(delegation::list_controllers)) + .route("/_delegation.addController", post(delegation::add_controller)) + .route("/_delegation.removeController", post(delegation::remove_controller)) + .route("/_delegation.updateControllerScopes", post(delegation::update_controller_scopes)) + .route("/_delegation.listControlledAccounts", get(delegation::list_controlled_accounts)) + .route("/_delegation.getAuditLog", get(delegation::get_audit_log)) + .route("/_delegation.getScopePresets", get(delegation::get_scope_presets)) + .route("/_delegation.createDelegatedAccount", post(delegation::create_delegated_account)) + .route("/_backup.listBackups", get(backup::list_backups)) + .route("/_backup.getBackup", get(backup::get_backup)) + .route("/_backup.createBackup", post(backup::create_backup)) + .route("/_backup.deleteBackup", post(backup::delete_backup)) + .route("/_backup.setEnabled", post(backup::set_backup_enabled)) + .route("/_backup.exportBlobs", get(backup::export_blobs)) + .route("/app.bsky.ageassurance.getState", get(age_assurance::get_state)) + .route("/app.bsky.unspecced.getAgeAssuranceState", get(age_assurance::get_age_assurance_state)) +} + +pub fn well_known_api_routes() -> axum::Router { + use axum::routing::get; + + axum::Router::new() + .route("/did.json", get(identity::well_known_did)) + .route("/atproto-did", get(identity::well_known_atproto_did)) +} + +pub fn webhook_routes() -> axum::Router { + use axum::{extract::DefaultBodyLimit, routing::post}; + + axum::Router::new() + .route( + "/webhook/telegram", + post(telegram_webhook::handle_telegram_webhook) + .layer(DefaultBodyLimit::max(64 * 1024)), + ) + .route( + "/webhook/discord", + post(discord_webhook::handle_discord_webhook) + .layer(DefaultBodyLimit::max(64 * 1024)), + ) +} + +pub fn misc_routes() -> axum::Router { + use axum::routing::get; + + axum::Router::new() + .route("/health", get(server::health)) + .route("/robots.txt", get(server::robots_txt)) + .route("/favicon.ico", get(server::get_logo)) + .route("/u/{handle}/did.json", get(identity::user_did_doc)) +} diff --git a/crates/tranquil-pds/src/api/moderation/mod.rs b/crates/tranquil-api/src/moderation/mod.rs similarity index 95% rename from crates/tranquil-pds/src/api/moderation/mod.rs rename to crates/tranquil-api/src/moderation/mod.rs index 69d6722..8388c8c 100644 --- a/crates/tranquil-pds/src/api/moderation/mod.rs +++ b/crates/tranquil-api/src/moderation/mod.rs @@ -1,7 +1,7 @@ -use crate::api::ApiError; -use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; -use crate::auth::{AnyUser, Auth}; -use crate::state::AppState; +use tranquil_pds::api::ApiError; +use tranquil_pds::api::proxy_client::{is_ssrf_safe, proxy_client}; +use tranquil_pds::auth::{AnyUser, Auth}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -94,7 +94,7 @@ pub async fn create_report( async fn proxy_to_report_service( state: &AppState, - auth_user: &crate::auth::AuthenticatedUser, + auth_user: &tranquil_pds::auth::AuthenticatedUser, service_url: &str, service_did: &str, input: &CreateReportInput, @@ -109,7 +109,7 @@ async fn proxy_to_report_service( Some(kb) => kb.clone(), None => match state.user_repo.get_with_key_by_did(&auth_user.did).await { Ok(Some(user_with_key)) => { - match crate::config::decrypt_key( + match tranquil_pds::config::decrypt_key( &user_with_key.key_bytes, user_with_key.encryption_version, ) { @@ -135,7 +135,7 @@ async fn proxy_to_report_service( }, }; - let service_token = match crate::auth::create_service_token( + let service_token = match tranquil_pds::auth::create_service_token( &auth_user.did, service_did, "com.atproto.moderation.createReport", @@ -211,7 +211,7 @@ async fn proxy_to_report_service( async fn create_report_locally( state: &AppState, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, is_takendown: bool, input: CreateReportInput, ) -> Response { diff --git a/crates/tranquil-pds/src/api/notification_prefs.rs b/crates/tranquil-api/src/notification_prefs.rs similarity index 93% rename from crates/tranquil-pds/src/api/notification_prefs.rs rename to crates/tranquil-api/src/notification_prefs.rs index 763b3f9..e94a759 100644 --- a/crates/tranquil-pds/src/api/notification_prefs.rs +++ b/crates/tranquil-api/src/notification_prefs.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::auth::{Active, Auth}; -use crate::state::AppState; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -142,14 +142,14 @@ pub async fn request_channel_verification( handle: Option<&str>, ) -> Result { let token = - crate::auth::verification_token::generate_channel_update_token(did, channel, identifier); - let formatted_token = crate::auth::verification_token::format_token_for_display(&token); + tranquil_pds::auth::verification_token::generate_channel_update_token(did, channel, identifier); + let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token); match channel { CommsChannel::Email => { let hostname = &tranquil_config::get().server.hostname; let handle_str = handle.unwrap_or("user"); - crate::comms::comms_repo::enqueue_email_update( + tranquil_pds::comms::comms_repo::enqueue_email_update( state.infra_repo.as_ref(), user_id, identifier, @@ -183,12 +183,12 @@ pub async fn request_channel_verification( .as_ref() .and_then(|p| p.preferred_locale.as_deref()) .unwrap_or("en"); - let strings = crate::comms::get_strings(locale); - let body = crate::comms::format_message( + let strings = tranquil_pds::comms::get_strings(locale); + let body = tranquil_pds::comms::format_message( strings.channel_verification_body, &[("code", &formatted_token), ("verify_link", &verify_link)], ); - let subject = crate::comms::format_message( + let subject = tranquil_pds::comms::format_message( strings.channel_verification_subject, &[("hostname", hostname)], ); @@ -277,7 +277,7 @@ pub async fn update_notification_prefs( return Err(ApiError::InvalidRequest("Email cannot be empty".into())); } - if !crate::api::validation::is_valid_email(&email_clean) { + if !tranquil_pds::api::validation::is_valid_email(&email_clean) { return Err(ApiError::InvalidEmail); } @@ -310,7 +310,7 @@ pub async fn update_notification_prefs( .await .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; info!(did = %auth.did, "Cleared Discord"); - } else if !crate::api::validation::is_valid_discord_username(&discord_clean) { + } else if !tranquil_pds::api::validation::is_valid_discord_username(&discord_clean) { return Err(ApiError::InvalidRequest( "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)" .into(), @@ -340,7 +340,7 @@ pub async fn update_notification_prefs( .await .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; info!(did = %auth.did, "Cleared Telegram username"); - } else if !crate::api::validation::is_valid_telegram_username(telegram_clean) { + } else if !tranquil_pds::api::validation::is_valid_telegram_username(telegram_clean) { return Err(ApiError::InvalidRequest( "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore" .into(), @@ -370,7 +370,7 @@ pub async fn update_notification_prefs( .await .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; info!(did = %auth.did, "Cleared Signal username"); - } else if !crate::comms::is_valid_signal_username(&signal_clean) { + } else if !tranquil_pds::comms::is_valid_signal_username(&signal_clean) { return Err(ApiError::InvalidRequest( "Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)" .into(), diff --git a/crates/tranquil-pds/src/api/repo/blob.rs b/crates/tranquil-api/src/repo/blob.rs similarity index 96% rename from crates/tranquil-pds/src/api/repo/blob.rs rename to crates/tranquil-api/src/repo/blob.rs index 67be8f9..7e4d9cf 100644 --- a/crates/tranquil-pds/src/api/repo/blob.rs +++ b/crates/tranquil-api/src/repo/blob.rs @@ -1,9 +1,9 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope}; -use crate::delegation::DelegationActionType; -use crate::state::AppState; -use crate::types::{CidLink, Did}; -use crate::util::get_header_str; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope}; +use tranquil_pds::delegation::DelegationActionType; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{CidLink, Did}; +use tranquil_pds::util::get_header_str; use axum::body::Body; use axum::{ Json, diff --git a/crates/tranquil-pds/src/api/repo/import.rs b/crates/tranquil-api/src/repo/import.rs similarity index 93% rename from crates/tranquil-pds/src/api/repo/import.rs rename to crates/tranquil-api/src/repo/import.rs index 119d0f4..2e6ab07 100644 --- a/crates/tranquil-pds/src/api/repo/import.rs +++ b/crates/tranquil-api/src/repo/import.rs @@ -1,11 +1,11 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::api::repo::record::create_signed_commit; -use crate::auth::{Auth, NotTakendown}; -use crate::state::AppState; -use crate::sync::import::{ImportError, apply_import, parse_car}; -use crate::sync::verify::CarVerifier; -use crate::types::Did; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::repo_ops::create_signed_commit; +use tranquil_pds::auth::{Auth, NotTakendown}; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::import::{ImportError, apply_import, parse_car}; +use tranquil_pds::sync::verify::CarVerifier; +use tranquil_pds::types::Did; use axum::{ body::Bytes, extract::State, @@ -121,7 +121,7 @@ pub async fn import_repo( verified.rev, verified.data_cid ); } - Err(crate::sync::verify::VerifyError::DidMismatch { + Err(tranquil_pds::sync::verify::VerifyError::DidMismatch { commit_did, expected_did, }) => { @@ -130,7 +130,7 @@ pub async fn import_repo( commit_did, expected_did ))); } - Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { + Err(tranquil_pds::sync::verify::VerifyError::MstValidationFailed(msg)) => { return Err(ApiError::InvalidRequest(format!( "MST validation failed: {}", msg @@ -154,7 +154,7 @@ pub async fn import_repo( verified.rev, verified.data_cid ); } - Err(crate::sync::verify::VerifyError::DidMismatch { + Err(tranquil_pds::sync::verify::VerifyError::DidMismatch { commit_did, expected_did, }) => { @@ -163,24 +163,24 @@ pub async fn import_repo( commit_did, expected_did ))); } - Err(crate::sync::verify::VerifyError::InvalidSignature) => { + Err(tranquil_pds::sync::verify::VerifyError::InvalidSignature) => { return Err(ApiError::InvalidRequest( "CAR file commit signature verification failed".into(), )); } - Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { + Err(tranquil_pds::sync::verify::VerifyError::DidResolutionFailed(msg)) => { warn!("DID resolution failed during import verification: {}", msg); return Err(ApiError::InvalidRequest(format!( "Failed to verify DID: {}", msg ))); } - Err(crate::sync::verify::VerifyError::NoSigningKey) => { + Err(tranquil_pds::sync::verify::VerifyError::NoSigningKey) => { return Err(ApiError::InvalidRequest( "DID document does not contain a signing key".into(), )); } - Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { + Err(tranquil_pds::sync::verify::VerifyError::MstValidationFailed(msg)) => { return Err(ApiError::InvalidRequest(format!( "MST validation failed: {}", msg @@ -264,7 +264,7 @@ pub async fn import_repo( ApiError::InternalError(Some("Signing key not found".into())) })?; let key_bytes = - crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) + tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) .map_err(|e| { error!("Failed to decrypt signing key: {}", e); ApiError::InternalError(None) diff --git a/crates/tranquil-pds/src/api/repo/meta.rs b/crates/tranquil-api/src/repo/meta.rs similarity index 88% rename from crates/tranquil-pds/src/api/repo/meta.rs rename to crates/tranquil-api/src/repo/meta.rs index 25726fd..c8d5edd 100644 --- a/crates/tranquil-pds/src/api/repo/meta.rs +++ b/crates/tranquil-api/src/repo/meta.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::state::AppState; -use crate::types::AtIdentifier; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; +use tranquil_pds::types::AtIdentifier; use axum::{ Json, extract::{Query, State}, @@ -20,7 +20,7 @@ pub async fn describe_repo( ) -> Response { let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); let user_row = if input.repo.is_did() { - let did: crate::types::Did = match input.repo.as_str().parse() { + let did: tranquil_pds::types::Did = match input.repo.as_str().parse() { Ok(d) => d, Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), }; @@ -36,7 +36,7 @@ pub async fn describe_repo( } else { repo_str.to_string() }; - let handle: crate::types::Handle = match handle_str.parse() { + let handle: tranquil_pds::types::Handle = match handle_str.parse() { Ok(h) => h, Err(_) => { return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); diff --git a/crates/tranquil-pds/src/api/repo/mod.rs b/crates/tranquil-api/src/repo/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/repo/mod.rs rename to crates/tranquil-api/src/repo/mod.rs diff --git a/crates/tranquil-pds/src/api/repo/record/batch.rs b/crates/tranquil-api/src/repo/record/batch.rs similarity index 96% rename from crates/tranquil-pds/src/api/repo/record/batch.rs rename to crates/tranquil-api/src/repo/record/batch.rs index 0ccb143..6431653 100644 --- a/crates/tranquil-pds/src/api/repo/record/batch.rs +++ b/crates/tranquil-api/src/repo/record/batch.rs @@ -1,17 +1,17 @@ use super::validation::validate_record_with_status; use super::validation_mode::{ValidationMode, deserialize_validation_mode}; -use crate::api::error::ApiError; -use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; -use crate::auth::{ +use tranquil_pds::api::error::ApiError; +use crate::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; +use tranquil_pds::auth::{ Active, Auth, WriteOpKind, require_not_migrated, require_verified_or_delegated, verify_batch_write_scopes, }; -use crate::cid_types::CommitCid; -use crate::delegation::DelegationActionType; -use crate::repo::tracking::TrackingBlockStore; -use crate::state::AppState; -use crate::types::{AtIdentifier, AtUri, Did, Nsid, Rkey}; -use crate::validation::ValidationStatus; +use tranquil_pds::cid_types::CommitCid; +use tranquil_pds::delegation::DelegationActionType; +use tranquil_pds::repo::tracking::TrackingBlockStore; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey}; +use tranquil_pds::validation::ValidationStatus; use axum::{ Json, extract::State, @@ -74,7 +74,7 @@ async fn process_single_write( }; all_blob_cids.extend(extract_blob_cids(value)); let rkey = rkey.clone().unwrap_or_else(Rkey::generate); - let record_ipld = crate::util::json_to_ipld(value); + let record_ipld = tranquil_pds::util::json_to_ipld(value); let record_bytes = serde_ipld_dagcbor::to_vec(&record_ipld).map_err(|_| { ApiError::InvalidRecord("Failed to serialize record".into()).into_response() })?; @@ -126,7 +126,7 @@ async fn process_single_write( } }; all_blob_cids.extend(extract_blob_cids(value)); - let record_ipld = crate::util::json_to_ipld(value); + let record_ipld = tranquil_pds::util::json_to_ipld(value); let record_bytes = serde_ipld_dagcbor::to_vec(&record_ipld).map_err(|_| { ApiError::InvalidRecord("Failed to serialize record".into()).into_response() })?; diff --git a/crates/tranquil-pds/src/api/repo/record/delete.rs b/crates/tranquil-api/src/repo/record/delete.rs similarity index 95% rename from crates/tranquil-pds/src/api/repo/record/delete.rs rename to crates/tranquil-api/src/repo/record/delete.rs index 36436ab..737f64b 100644 --- a/crates/tranquil-pds/src/api/repo/record/delete.rs +++ b/crates/tranquil-api/src/repo/record/delete.rs @@ -1,14 +1,14 @@ -use crate::api::error::ApiError; -use crate::api::repo::record::utils::{ +use tranquil_pds::api::error::ApiError; +use crate::repo::record::utils::{ CommitError, CommitParams, RecordOp, commit_and_log, get_current_root_cid, }; -use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; -use crate::auth::{Active, Auth, VerifyScope}; -use crate::cid_types::CommitCid; -use crate::delegation::DelegationActionType; -use crate::repo::tracking::TrackingBlockStore; -use crate::state::AppState; -use crate::types::{AtIdentifier, AtUri, Nsid, Rkey}; +use crate::repo::record::write::{CommitInfo, prepare_repo_write}; +use tranquil_pds::auth::{Active, Auth, VerifyScope}; +use tranquil_pds::cid_types::CommitCid; +use tranquil_pds::delegation::DelegationActionType; +use tranquil_pds::repo::tracking::TrackingBlockStore; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{AtIdentifier, AtUri, Nsid, Rkey}; use axum::{ Json, extract::State, @@ -45,7 +45,7 @@ pub async fn delete_record( State(state): State, auth: Auth, Json(input): Json, -) -> Result { +) -> Result { let scope_proof = match auth.verify_repo_delete(&input.collection) { Ok(proof) => proof, Err(e) => return Ok(e.into_response()), @@ -229,7 +229,7 @@ pub async fn delete_record( .into_response()) } -use crate::types::Did; +use tranquil_pds::types::Did; use uuid::Uuid; pub async fn delete_record_internal( diff --git a/crates/tranquil-pds/src/api/repo/record/mod.rs b/crates/tranquil-api/src/repo/record/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/repo/record/mod.rs rename to crates/tranquil-api/src/repo/record/mod.rs diff --git a/crates/tranquil-pds/src/api/repo/record/pagination.rs b/crates/tranquil-api/src/repo/record/pagination.rs similarity index 100% rename from crates/tranquil-pds/src/api/repo/record/pagination.rs rename to crates/tranquil-api/src/repo/record/pagination.rs diff --git a/crates/tranquil-pds/src/api/repo/record/read.rs b/crates/tranquil-api/src/repo/record/read.rs similarity index 94% rename from crates/tranquil-pds/src/api/repo/record/read.rs rename to crates/tranquil-api/src/repo/record/read.rs index c502060..f95bb9d 100644 --- a/crates/tranquil-pds/src/api/repo/record/read.rs +++ b/crates/tranquil-api/src/repo/record/read.rs @@ -1,7 +1,7 @@ use super::pagination::{PaginationDirection, deserialize_pagination_direction}; -use crate::api::error::ApiError; -use crate::state::AppState; -use crate::types::{AtIdentifier, Nsid, Rkey}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{AtIdentifier, Nsid, Rkey}; use axum::{ Json, extract::{Query, State}, @@ -61,7 +61,7 @@ pub async fn get_record( ) -> Response { let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); let user_id_opt = if input.repo.is_did() { - let did: crate::types::Did = match input.repo.as_str().parse() { + let did: tranquil_pds::types::Did = match input.repo.as_str().parse() { Ok(d) => d, Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), }; @@ -73,7 +73,7 @@ pub async fn get_record( } else { repo_str.to_string() }; - let handle: crate::types::Handle = match handle_str.parse() { + let handle: tranquil_pds::types::Handle = match handle_str.parse() { Ok(h) => h, Err(_) => { return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); @@ -160,7 +160,7 @@ pub async fn list_records( ) -> Response { let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); let user_id_opt = if input.repo.is_did() { - let did: crate::types::Did = match input.repo.as_str().parse() { + let did: tranquil_pds::types::Did = match input.repo.as_str().parse() { Ok(d) => d, Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), }; @@ -172,7 +172,7 @@ pub async fn list_records( } else { repo_str.to_string() }; - let handle: crate::types::Handle = match handle_str.parse() { + let handle: tranquil_pds::types::Handle = match handle_str.parse() { Ok(h) => h, Err(_) => { return ApiError::InvalidRequest("Invalid handle format".into()).into_response(); @@ -198,7 +198,7 @@ pub async fn list_records( let cursor_rkey = input .cursor .as_ref() - .and_then(|c| c.parse::().ok()); + .and_then(|c| c.parse::().ok()); let rows = match state .repo_repo .list_records( diff --git a/crates/tranquil-api/src/repo/record/utils.rs b/crates/tranquil-api/src/repo/record/utils.rs new file mode 100644 index 0000000..44088b7 --- /dev/null +++ b/crates/tranquil-api/src/repo/record/utils.rs @@ -0,0 +1 @@ +pub use tranquil_pds::repo_ops::*; diff --git a/crates/tranquil-pds/src/api/repo/record/validation.rs b/crates/tranquil-api/src/repo/record/validation.rs similarity index 91% rename from crates/tranquil-pds/src/api/repo/record/validation.rs rename to crates/tranquil-api/src/repo/record/validation.rs index e49686c..15ce69b 100644 --- a/crates/tranquil-pds/src/api/repo/record/validation.rs +++ b/crates/tranquil-api/src/repo/record/validation.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::types::{Nsid, Rkey}; -use crate::validation::{RecordValidator, ValidationError, ValidationStatus}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::types::{Nsid, Rkey}; +use tranquil_pds::validation::{RecordValidator, ValidationError, ValidationStatus}; use axum::response::Response; pub async fn validate_record_with_status( diff --git a/crates/tranquil-pds/src/api/repo/record/validation_mode.rs b/crates/tranquil-api/src/repo/record/validation_mode.rs similarity index 100% rename from crates/tranquil-pds/src/api/repo/record/validation_mode.rs rename to crates/tranquil-api/src/repo/record/validation_mode.rs diff --git a/crates/tranquil-pds/src/api/repo/record/write.rs b/crates/tranquil-api/src/repo/record/write.rs similarity index 96% rename from crates/tranquil-pds/src/api/repo/record/write.rs rename to crates/tranquil-api/src/repo/record/write.rs index 9927c20..faaecd9 100644 --- a/crates/tranquil-pds/src/api/repo/record/write.rs +++ b/crates/tranquil-api/src/repo/record/write.rs @@ -1,20 +1,20 @@ use super::validation::validate_record_with_status; use super::validation_mode::{ValidationMode, deserialize_validation_mode}; -use crate::api::error::ApiError; -use crate::api::repo::record::utils::{ +use tranquil_pds::api::error::ApiError; +use crate::repo::record::utils::{ CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, get_current_root_cid, }; -use crate::auth::{ +use tranquil_pds::auth::{ Active, Auth, AuthSource, RepoScopeAction, ScopeVerified, VerifyScope, require_not_migrated, require_verified_or_delegated, }; -use crate::cid_types::CommitCid; -use crate::delegation::DelegationActionType; -use crate::repo::tracking::TrackingBlockStore; -use crate::state::AppState; -use crate::types::{AtIdentifier, AtUri, Did, Nsid, Rkey}; -use crate::validation::ValidationStatus; +use tranquil_pds::cid_types::CommitCid; +use tranquil_pds::delegation::DelegationActionType; +use tranquil_pds::repo::tracking::TrackingBlockStore; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey}; +use tranquil_pds::validation::ValidationStatus; use axum::{ Json, extract::State, @@ -104,7 +104,7 @@ pub async fn create_record( State(state): State, auth: Auth, Json(input): Json, -) -> Result { +) -> Result { let scope_proof = match auth.verify_repo_create(&input.collection) { Ok(proof) => proof, Err(e) => return Ok(e.into_response()), @@ -232,7 +232,7 @@ pub async fn create_record( } } - let record_ipld = crate::util::json_to_ipld(&input.record); + let record_ipld = tranquil_pds::util::json_to_ipld(&input.record); let mut record_bytes = Vec::new(); if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); @@ -408,7 +408,7 @@ pub async fn put_record( State(state): State, auth: Auth, Json(input): Json, -) -> Result { +) -> Result { let upsert_proof = match auth.verify_repo_upsert(&input.collection) { Ok(proof) => proof, Err(e) => return Ok(e.into_response()), @@ -476,7 +476,7 @@ pub async fn put_record( } } let existing_cid = mst.get(&key).await.ok().flatten(); - let record_ipld = crate::util::json_to_ipld(&input.record); + let record_ipld = tranquil_pds::util::json_to_ipld(&input.record); let mut record_bytes = Vec::new(); if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); diff --git a/crates/tranquil-pds/src/api/server/account_status.rs b/crates/tranquil-api/src/server/account_status.rs similarity index 91% rename from crates/tranquil-pds/src/api/server/account_status.rs rename to crates/tranquil-api/src/server/account_status.rs index 0bbb493..54c14a3 100644 --- a/crates/tranquil-pds/src/api/server/account_status.rs +++ b/crates/tranquil-api/src/server/account_status.rs @@ -1,10 +1,10 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Auth, NotTakendown, Permissive, require_legacy_session_mfa}; -use crate::cache::Cache; -use crate::plc::PlcClient; -use crate::state::AppState; -use crate::types::PlainPassword; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Auth, NotTakendown, Permissive, require_legacy_session_mfa}; +use tranquil_pds::cache::Cache; +use tranquil_pds::plc::PlcClient; +use tranquil_pds::state::AppState; +use tranquil_pds::types::PlainPassword; use axum::{ Json, extract::State, @@ -117,7 +117,7 @@ pub async fn check_account_status( async fn is_valid_did_for_service( user_repo: &dyn tranquil_db_traits::UserRepository, cache: Arc, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> bool { assert_valid_did_document_for_service(user_repo, cache, did, false) .await @@ -127,7 +127,7 @@ async fn is_valid_did_for_service( async fn assert_valid_did_document_for_service( user_repo: &dyn tranquil_db_traits::UserRepository, cache: Arc, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, with_retry: bool, ) -> Result<(), ApiError> { let hostname = &tranquil_config::get().server.hostname; @@ -226,7 +226,7 @@ async fn assert_valid_did_document_for_service( if let Some(key_info) = user_key { let key_bytes = - crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) + tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) .map_err(|e| { error!("Failed to decrypt user key: {}", e); ApiError::InternalError(None) @@ -235,7 +235,7 @@ async fn assert_valid_did_document_for_service( error!("Failed to create signing key: {:?}", e); ApiError::InternalError(None) })?; - let expected_did_key = crate::plc::signing_key_to_did_key(&signing_key); + let expected_did_key = tranquil_pds::plc::signing_key_to_did_key(&signing_key); if doc_signing_key != Some(&expected_did_key) { warn!( @@ -248,7 +248,7 @@ async fn assert_valid_did_document_for_service( } } } else if let Some(host_and_path) = did.as_str().strip_prefix("did:web:") { - let client = crate::api::proxy_client::did_resolution_client(); + let client = tranquil_pds::api::proxy_client::did_resolution_client(); let decoded = host_and_path.replace("%3A", ":"); let parts: Vec<&str> = decoded.split(':').collect(); let (host, path_parts) = if parts.len() > 1 && parts[1].chars().all(|c| c.is_ascii_digit()) @@ -284,7 +284,7 @@ async fn assert_valid_did_document_for_service( arr.iter().find(|svc| { svc.get("id").and_then(|id| id.as_str()) == Some("#atproto_pds") || svc.get("type").and_then(|t| t.as_str()) - == Some(crate::plc::ServiceType::Pds.as_str()) + == Some(tranquil_pds::plc::ServiceType::Pds.as_str()) }) }) .and_then(|svc| svc.get("serviceEndpoint")) @@ -314,11 +314,11 @@ pub async fn activate_account( auth.did ); - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Repo, - crate::oauth::scopes::AccountAction::Manage, + tranquil_pds::oauth::scopes::AccountAttr::Repo, + tranquil_pds::oauth::scopes::AccountAction::Manage, ) { info!("[MIGRATION] activateAccount: Scope check failed"); return Ok(e); @@ -365,15 +365,15 @@ pub async fn activate_account( did ); if let Some(ref h) = handle { - let _ = state.cache.delete(&crate::cache_keys::handle_key(h)).await; + let _ = state.cache.delete(&tranquil_pds::cache_keys::handle_key(h)).await; } let _ = state .cache - .delete(&crate::cache_keys::plc_doc_key(&did)) + .delete(&tranquil_pds::cache_keys::plc_doc_key(&did)) .await; let _ = state .cache - .delete(&crate::cache_keys::plc_data_key(&did)) + .delete(&tranquil_pds::cache_keys::plc_data_key(&did)) .await; if state.did_resolver.refresh_did(did.as_str()).await.is_none() { warn!( @@ -385,7 +385,7 @@ pub async fn activate_account( "[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}", did ); - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did, tranquil_db_traits::AccountStatus::Active, @@ -404,7 +404,7 @@ pub async fn activate_account( did, handle ); let handle_typed = handle.clone(); - if let Err(e) = crate::api::repo::record::sequence_identity_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event( &state, &did, handle_typed.as_ref(), @@ -438,7 +438,7 @@ pub async fn activate_account( } else { None }; - if let Err(e) = crate::api::repo::record::sequence_sync_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event( &state, &did, root_cid_link.as_str(), @@ -483,11 +483,11 @@ pub async fn deactivate_account( auth: Auth, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Repo, - crate::oauth::scopes::AccountAction::Manage, + tranquil_pds::oauth::scopes::AccountAttr::Repo, + tranquil_pds::oauth::scopes::AccountAction::Manage, ) { return Ok(e); } @@ -507,9 +507,9 @@ pub async fn deactivate_account( match result { Ok(true) => { if let Some(ref h) = handle { - let _ = state.cache.delete(&crate::cache_keys::handle_key(h)).await; + let _ = state.cache.delete(&tranquil_pds::cache_keys::handle_key(h)).await; } - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did, tranquil_db_traits::AccountStatus::Deactivated, @@ -552,7 +552,7 @@ pub async fn request_account_delete( .await .log_db_err("creating deletion token")?; let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_account_deletion( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -569,7 +569,7 @@ pub async fn request_account_delete( #[derive(Deserialize)] pub struct DeleteAccountInput { - pub did: crate::types::Did, + pub did: tranquil_pds::types::Did, pub password: PlainPassword, pub token: String, } @@ -642,7 +642,7 @@ pub async fn delete_account( error!("DB error deleting account: {:?}", e); return ApiError::InternalError(None).into_response(); } - let account_seq = crate::api::repo::record::sequence_account_event( + let account_seq = tranquil_pds::repo_ops::sequence_account_event( &state, did, tranquil_db_traits::AccountStatus::Deleted, @@ -666,7 +666,7 @@ pub async fn delete_account( } let _ = state .cache - .delete(&crate::cache_keys::handle_key(&handle)) + .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; info!("Account {} deleted successfully", did); EmptyResponse::ok().into_response() diff --git a/crates/tranquil-pds/src/api/server/app_password.rs b/crates/tranquil-api/src/server/app_password.rs similarity index 94% rename from crates/tranquil-pds/src/api/server/app_password.rs rename to crates/tranquil-api/src/server/app_password.rs index 89732aa..f37dfaa 100644 --- a/crates/tranquil-pds/src/api/server/app_password.rs +++ b/crates/tranquil-api/src/server/app_password.rs @@ -1,9 +1,9 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Auth, NotTakendown, Permissive, generate_app_password}; -use crate::delegation::{DelegationActionType, intersect_scopes}; -use crate::rate_limit::{AppPasswordLimit, RateLimited}; -use crate::state::AppState; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Auth, NotTakendown, Permissive, generate_app_password}; +use tranquil_pds::delegation::{DelegationActionType, intersect_scopes}; +use tranquil_pds::rate_limit::{AppPasswordLimit, RateLimited}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -233,7 +233,7 @@ pub async fn revoke_app_password( .log_db_err("revoking sessions for app password")?; futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { - let cache_key = crate::cache_keys::session_key(&auth.did, jti); + let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, jti); let cache = state.cache.clone(); async move { let _ = cache.delete(&cache_key).await; diff --git a/crates/tranquil-pds/src/api/server/email.rs b/crates/tranquil-api/src/server/email.rs similarity index 84% rename from crates/tranquil-pds/src/api/server/email.rs rename to crates/tranquil-api/src/server/email.rs index 44829c5..2d943d5 100644 --- a/crates/tranquil-pds/src/api/server/email.rs +++ b/crates/tranquil-api/src/server/email.rs @@ -1,8 +1,8 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; -use crate::auth::{Auth, NotTakendown}; -use crate::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit}; -use crate::state::AppState; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; +use tranquil_pds::auth::{Auth, NotTakendown}; +use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -20,7 +20,7 @@ use tranquil_db_traits::CommsChannel; const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60); fn email_update_cache_key(did: &str) -> String { - crate::cache_keys::email_update_key(did) + tranquil_pds::cache_keys::email_update_key(did) } fn hash_token(token: &str) -> String { @@ -49,11 +49,11 @@ pub async fn request_email_update( auth: Auth, input: Option>, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Email, - crate::oauth::scopes::AccountAction::Manage, + tranquil_pds::oauth::scopes::AccountAttr::Email, + tranquil_pds::oauth::scopes::AccountAction::Manage, ) { return Ok(e); } @@ -74,10 +74,10 @@ pub async fn request_email_update( let token_required = user.email_verified; if token_required { - let token = crate::auth::email_token::create_email_token( + let token = tranquil_pds::auth::email_token::create_email_token( state.cache.as_ref(), auth.did.as_str(), - crate::auth::email_token::EmailTokenPurpose::UpdateEmail, + tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, ) .await .map_err(|e| { @@ -89,7 +89,7 @@ pub async fn request_email_update( && let Some(ref new_email) = inp.new_email { let new_email = new_email.trim().to_lowercase(); - if !new_email.is_empty() && crate::api::validation::is_valid_email(&new_email) { + if !new_email.is_empty() && tranquil_pds::api::validation::is_valid_email(&new_email) { let pending = PendingEmailUpdate { new_email, token_hash: hash_token(&token), @@ -105,7 +105,7 @@ pub async fn request_email_update( } let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_short_token_email( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_short_token_email( state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, @@ -135,11 +135,11 @@ pub async fn confirm_email( auth: Auth, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Email, - crate::oauth::scopes::AccountAction::Manage, + tranquil_pds::oauth::scopes::AccountAttr::Email, + tranquil_pds::oauth::scopes::AccountAction::Manage, ) { return Ok(e); } @@ -167,9 +167,9 @@ pub async fn confirm_email( } let confirmation_code = - crate::auth::verification_token::normalize_token_input(input.token.trim()); + tranquil_pds::auth::verification_token::normalize_token_input(input.token.trim()); - let verified = crate::auth::verification_token::verify_signup_token( + let verified = tranquil_pds::auth::verification_token::verify_signup_token( &confirmation_code, CommsChannel::Email, &provided_email, @@ -181,7 +181,7 @@ pub async fn confirm_email( return Err(ApiError::InvalidToken(None)); } } - Err(crate::auth::verification_token::VerifyError::Expired) => { + Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => { return Err(ApiError::ExpiredToken(None)); } Err(_) => { @@ -213,11 +213,11 @@ pub async fn update_email( auth: Auth, Json(input): Json, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Email, - crate::oauth::scopes::AccountAction::Manage, + tranquil_pds::oauth::scopes::AccountAttr::Email, + tranquil_pds::oauth::scopes::AccountAction::Manage, ) { return Ok(e); } @@ -235,7 +235,7 @@ pub async fn update_email( let email_verified = user.email_verified; let new_email = input.email.trim().to_lowercase(); - if !crate::api::validation::is_valid_email(&new_email) { + if !tranquil_pds::api::validation::is_valid_email(&new_email) { return Err(ApiError::InvalidRequest( "This email address is not supported, please use a different email.".into(), )); @@ -255,15 +255,15 @@ pub async fn update_email( .filter(|t| !t.is_empty()) .ok_or(ApiError::TokenRequired)?; - crate::auth::email_token::validate_email_token( + tranquil_pds::auth::email_token::validate_email_token( state.cache.as_ref(), did.as_str(), - crate::auth::email_token::EmailTokenPurpose::UpdateEmail, + tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, token, ) .await .map_err(|e| match e { - crate::auth::email_token::TokenError::ExpiredToken => { + tranquil_pds::auth::email_token::TokenError::ExpiredToken => { ApiError::ExpiredToken(None) } _ => ApiError::InvalidToken(None), @@ -303,24 +303,24 @@ pub async fn update_email( .filter(|t| !t.is_empty()) .ok_or(ApiError::TokenRequired)?; - let short_token_result = crate::auth::email_token::validate_email_token( + let short_token_result = tranquil_pds::auth::email_token::validate_email_token( state.cache.as_ref(), did.as_str(), - crate::auth::email_token::EmailTokenPurpose::UpdateEmail, + tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, token, ) .await; if let Err(e) = short_token_result { let confirmation_token = - crate::auth::verification_token::normalize_token_input(token.trim()); + tranquil_pds::auth::verification_token::normalize_token_input(token.trim()); let current_email_lower = current_email .as_ref() .map(|e| e.to_lowercase()) .unwrap_or_default(); - let verified = crate::auth::verification_token::verify_channel_update_token( + let verified = tranquil_pds::auth::verification_token::verify_channel_update_token( &confirmation_token, CommsChannel::Email, ¤t_email_lower, @@ -332,9 +332,9 @@ pub async fn update_email( return Err(ApiError::InvalidToken(None)); } } - Err(crate::auth::verification_token::VerifyError::Expired) => { + Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => { return Err(match e { - crate::auth::email_token::TokenError::ExpiredToken => { + tranquil_pds::auth::email_token::TokenError::ExpiredToken => { ApiError::ExpiredToken(None) } _ => ApiError::InvalidToken(None), @@ -342,7 +342,7 @@ pub async fn update_email( } Err(_) => { return Err(match e { - crate::auth::email_token::TokenError::ExpiredToken => { + tranquil_pds::auth::email_token::TokenError::ExpiredToken => { ApiError::ExpiredToken(None) } _ => ApiError::InvalidToken(None), @@ -359,15 +359,15 @@ pub async fn update_email( .await .log_db_err("updating email")?; - let verification_token = crate::auth::verification_token::generate_signup_token( + let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( did, CommsChannel::Email, &new_email, ); let formatted_token = - crate::auth::verification_token::format_token_for_display(&verification_token); + tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -423,7 +423,7 @@ pub async fn check_email_verified( #[derive(Deserialize)] pub struct CheckChannelVerifiedInput { - pub did: crate::types::Did, + pub did: tranquil_pds::types::Did, pub channel: CommsChannel, } @@ -456,11 +456,11 @@ pub async fn authorize_email_update( _rate_limit: RateLimited, axum::extract::Query(query): axum::extract::Query, ) -> Response { - let verified = crate::auth::verification_token::verify_token_signature(&query.token); + let verified = tranquil_pds::auth::verification_token::verify_token_signature(&query.token); let token_data = match verified { Ok(data) => data, - Err(crate::auth::verification_token::VerifyError::Expired) => { + Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => { warn!("authorize_email_update: token expired"); return ApiError::ExpiredToken(None).into_response(); } @@ -470,7 +470,7 @@ pub async fn authorize_email_update( } }; - if token_data.purpose != crate::auth::verification_token::VerificationPurpose::ChannelUpdate { + if token_data.purpose != tranquil_pds::auth::verification_token::VerificationPurpose::ChannelUpdate { warn!( "authorize_email_update: wrong purpose: {:?}", token_data.purpose @@ -544,11 +544,11 @@ pub async fn check_email_update_status( _rate_limit: RateLimited, auth: Auth, ) -> Result { - if let Err(e) = crate::auth::scope_check::check_account_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope( &auth.auth_source, auth.scope.as_deref(), - crate::oauth::scopes::AccountAttr::Email, - crate::oauth::scopes::AccountAction::Read, + tranquil_pds::oauth::scopes::AccountAttr::Email, + tranquil_pds::oauth::scopes::AccountAction::Read, ) { return Ok(e); } diff --git a/crates/tranquil-pds/src/api/server/invite.rs b/crates/tranquil-api/src/server/invite.rs similarity index 97% rename from crates/tranquil-pds/src/api/server/invite.rs rename to crates/tranquil-api/src/server/invite.rs index b904dac..5e85ee0 100644 --- a/crates/tranquil-pds/src/api/server/invite.rs +++ b/crates/tranquil-api/src/server/invite.rs @@ -1,8 +1,8 @@ -use crate::api::ApiError; -use crate::api::error::DbResultExt; -use crate::auth::{Admin, Auth, NotTakendown}; -use crate::state::AppState; -use crate::types::Did; +use tranquil_pds::api::ApiError; +use tranquil_pds::api::error::DbResultExt; +use tranquil_pds::auth::{Admin, Auth, NotTakendown}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::Did; use axum::{ Json, extract::State, diff --git a/crates/tranquil-pds/src/api/server/logo.rs b/crates/tranquil-api/src/server/logo.rs similarity index 94% rename from crates/tranquil-pds/src/api/server/logo.rs rename to crates/tranquil-api/src/server/logo.rs index 037c13e..be1f4c3 100644 --- a/crates/tranquil-pds/src/api/server/logo.rs +++ b/crates/tranquil-api/src/server/logo.rs @@ -1,4 +1,4 @@ -use crate::state::AppState; +use tranquil_pds::state::AppState; use axum::{ body::Body, extract::State, @@ -21,7 +21,7 @@ pub async fn get_logo(State(state): State) -> Response { Some(c) if !c.is_empty() => c, _ => return StatusCode::NOT_FOUND.into_response(), }; - let cid = match crate::types::CidLink::new(&cid_str) { + let cid = match tranquil_pds::types::CidLink::new(&cid_str) { Ok(c) => c, Err(_) => return StatusCode::NOT_FOUND.into_response(), }; diff --git a/crates/tranquil-pds/src/api/server/meta.rs b/crates/tranquil-api/src/server/meta.rs similarity index 95% rename from crates/tranquil-pds/src/api/server/meta.rs rename to crates/tranquil-api/src/server/meta.rs index 3a9531c..0bfa2f5 100644 --- a/crates/tranquil-pds/src/api/server/meta.rs +++ b/crates/tranquil-api/src/server/meta.rs @@ -1,6 +1,6 @@ -use crate::BUILD_VERSION; -use crate::state::AppState; -use crate::util::{discord_app_id, discord_bot_username, telegram_bot_username}; +use tranquil_pds::BUILD_VERSION; +use tranquil_pds::state::AppState; +use tranquil_pds::util::{discord_app_id, discord_bot_username, telegram_bot_username}; use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; use serde_json::json; diff --git a/crates/tranquil-pds/src/api/server/migration.rs b/crates/tranquil-api/src/server/migration.rs similarity index 92% rename from crates/tranquil-pds/src/api/server/migration.rs rename to crates/tranquil-api/src/server/migration.rs index 600a3a2..ef477eb 100644 --- a/crates/tranquil-pds/src/api/server/migration.rs +++ b/crates/tranquil-api/src/server/migration.rs @@ -1,7 +1,7 @@ -use crate::api::ApiError; -use crate::api::error::DbResultExt; -use crate::auth::{Active, Auth}; -use crate::state::AppState; +use tranquil_pds::api::ApiError; +use tranquil_pds::api::error::DbResultExt; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -145,7 +145,7 @@ pub async fn get_did_document( Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) } -async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value { +async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) -> serde_json::Value { let hostname = &tranquil_config::get().server.hostname; let user = match state.user_repo.get_user_for_did_doc_build(did).await { @@ -195,7 +195,7 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_ })).collect::>(), "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": service_endpoint }] }); @@ -209,8 +209,8 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_ .flatten(); let public_key_multibase = match key_info { - Some(info) => match crate::config::decrypt_key(&info.key_bytes, info.encryption_version) { - Ok(key_bytes) => crate::api::identity::did::get_public_key_multibase(&key_bytes) + Some(info) => match tranquil_pds::config::decrypt_key(&info.key_bytes, info.encryption_version) { + Ok(key_bytes) => crate::identity::did::get_public_key_multibase(&key_bytes) .unwrap_or_else(|_| "error".to_string()), Err(_) => "error".to_string(), }, @@ -243,7 +243,7 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_ }], "service": [{ "id": "#atproto_pds", - "type": crate::plc::ServiceType::Pds.as_str(), + "type": tranquil_pds::plc::ServiceType::Pds.as_str(), "serviceEndpoint": service_endpoint }] }) diff --git a/crates/tranquil-pds/src/api/server/mod.rs b/crates/tranquil-api/src/server/mod.rs similarity index 100% rename from crates/tranquil-pds/src/api/server/mod.rs rename to crates/tranquil-api/src/server/mod.rs diff --git a/crates/tranquil-pds/src/api/server/passkey_account.rs b/crates/tranquil-api/src/server/passkey_account.rs similarity index 92% rename from crates/tranquil-pds/src/api/server/passkey_account.rs rename to crates/tranquil-api/src/server/passkey_account.rs index 8c25946..f5acce3 100644 --- a/crates/tranquil-pds/src/api/server/passkey_account.rs +++ b/crates/tranquil-api/src/server/passkey_account.rs @@ -1,6 +1,6 @@ -use crate::api::SuccessResponse; -use crate::api::error::ApiError; -use crate::auth::NormalizedLoginIdentifier; +use tranquil_pds::api::SuccessResponse; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::NormalizedLoginIdentifier; use axum::{ Json, extract::State, @@ -19,12 +19,12 @@ use tracing::{debug, error, info, warn}; use tranquil_db_traits::WebauthnChallengeType; use uuid::Uuid; -use crate::api::repo::record::utils::create_signed_commit; -use crate::auth::{ServiceTokenVerifier, generate_app_password, is_service_token}; -use crate::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited}; -use crate::state::AppState; -use crate::types::{Did, Handle, PlainPassword}; -use crate::validation::validate_password; +use tranquil_pds::repo_ops::create_signed_commit; +use tranquil_pds::auth::{ServiceTokenVerifier, generate_app_password, is_service_token}; +use tranquil_pds::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle, PlainPassword}; +use tranquil_pds::validation::validate_password; fn generate_setup_token() -> String { let mut rng = rand::thread_rng(); @@ -72,8 +72,8 @@ pub async fn create_passkey_account( headers: HeaderMap, Json(input): Json, ) -> Response { - let byod_auth = if let Some(extracted) = crate::auth::extract_auth_token_from_header( - crate::util::get_header_str(&headers, http::header::AUTHORIZATION), + let byod_auth = if let Some(extracted) = tranquil_pds::auth::extract_auth_token_from_header( + tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION), ) { let token = extracted.token; if is_service_token(&token) { @@ -128,14 +128,14 @@ pub async fn create_passkey_account( .unwrap_or(&input.handle), None => &input.handle, }; - match crate::api::validation::validate_short_handle(handle_to_validate) { + match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])), Err(_) => { return ApiError::InvalidHandle(None).into_response(); } } } else { - match crate::api::validation::validate_full_domain_handle(&input.handle) { + match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { Ok(h) => h, Err(_) => return ApiError::InvalidHandle(None).into_response(), } @@ -147,7 +147,7 @@ pub async fn create_passkey_account( .map(|e| e.trim().to_string()) .filter(|e| !e.is_empty()); if let Some(ref email) = email - && !crate::api::validation::is_valid_email(email) + && !tranquil_pds::api::validation::is_valid_email(email) { return ApiError::InvalidEmail.into_response(); } @@ -184,7 +184,7 @@ pub async fn create_passkey_account( tranquil_db_traits::CommsChannel::Discord => match &input.discord_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().to_lowercase(); - if !crate::api::validation::is_valid_discord_username(&clean) { + if !tranquil_pds::api::validation::is_valid_discord_username(&clean) { return ApiError::InvalidRequest( "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(), ).into_response(); @@ -196,7 +196,7 @@ pub async fn create_passkey_account( tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().trim_start_matches('@'); - if !crate::api::validation::is_valid_telegram_username(clean) { + if !tranquil_pds::api::validation::is_valid_telegram_username(clean) { return ApiError::InvalidRequest( "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(), ).into_response(); @@ -250,7 +250,7 @@ pub async fn create_passkey_account( let did = match did_type { "web" => { - if !crate::api::server::meta::is_self_hosted_did_web_enabled() { + if !tranquil_pds::util::is_self_hosted_did_web_enabled() { return ApiError::SelfHostedDidWebDisabled.into_response(); } let encoded_handle = handle.replace(':', "%3A"); @@ -284,7 +284,7 @@ pub async fn create_passkey_account( } info!(did = %d, "Creating external did:web passkey account (BYOD key)"); } else { - if let Err(e) = crate::api::identity::did::verify_did_web( + if let Err(e) = crate::identity::did::verify_did_web( d, hostname, &input.handle, @@ -328,9 +328,9 @@ pub async fn create_passkey_account( .secrets .plc_rotation_key .clone() - .unwrap_or_else(|| crate::plc::signing_key_to_did_key(&secret_key)); + .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&secret_key)); - let genesis_result = match crate::plc::create_genesis_operation( + let genesis_result = match tranquil_pds::plc::create_genesis_operation( &secret_key, &rotation_key, &handle, @@ -346,7 +346,7 @@ pub async fn create_passkey_account( } }; - let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone())); + let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone())); if let Err(e) = plc_client .send_operation(&genesis_result.did, &genesis_result.signed_operation) .await @@ -381,7 +381,7 @@ pub async fn create_passkey_account( None }; - let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { + let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) { Ok(bytes) => bytes, Err(e) => { error!("Error encrypting signing key: {:?}", e); @@ -458,7 +458,7 @@ pub async fn create_passkey_account( setup_expires_at, deactivated_at, encrypted_key_bytes, - encryption_version: crate::config::ENCRYPTION_VERSION, + encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, reserved_key_id, commit_cid: commit_cid.to_string(), repo_rev: rev.as_ref().to_string(), @@ -487,7 +487,7 @@ pub async fn create_passkey_account( let user_id = create_result.user_id; if !is_byod_did_web { - if let Err(e) = crate::api::repo::record::sequence_identity_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event( &state, &did_typed, Some(&handle_typed), @@ -496,7 +496,7 @@ pub async fn create_passkey_account( { warn!("Failed to sequence identity event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did_typed, tranquil_db_traits::AccountStatus::Active, @@ -509,11 +509,11 @@ pub async fn create_passkey_account( "$type": "app.bsky.actor.profile", "displayName": handle }); - if let Err(e) = crate::api::repo::record::create_record_internal( + if let Err(e) = tranquil_pds::repo_ops::create_record_internal( &state, &did_typed, - &crate::types::PROFILE_COLLECTION, - &crate::types::PROFILE_RKEY, + &tranquil_pds::types::PROFILE_COLLECTION, + &tranquil_pds::types::PROFILE_RKEY, &profile_record, ) .await @@ -522,14 +522,14 @@ pub async fn create_passkey_account( } } - let verification_token = crate::auth::verification_token::generate_signup_token( + let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( &did_typed, verification_channel, &verification_recipient, ); let formatted_token = - crate::auth::verification_token::format_token_for_display(&verification_token); - if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( + tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -546,7 +546,7 @@ pub async fn create_passkey_account( info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion"); let access_jwt = if byod_auth.is_some() { - match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { + match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) { Ok(token_meta) => { let refresh_jti = uuid::Uuid::new_v4().to_string(); let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24); @@ -887,7 +887,7 @@ pub async fn request_passkey_recovery( urlencoding::encode(&recovery_token) ); - let _ = crate::comms::comms_repo::enqueue_passkey_recovery( + let _ = tranquil_pds::comms::comms_repo::enqueue_passkey_recovery( state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, @@ -968,7 +968,7 @@ pub async fn recover_passkey_account( } if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user.id).await { let actual_channel = - crate::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel); + tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel); if let Err(e) = state .user_repo .set_channel_verified(&input.did, actual_channel) diff --git a/crates/tranquil-pds/src/api/server/passkeys.rs b/crates/tranquil-api/src/server/passkeys.rs similarity index 96% rename from crates/tranquil-pds/src/api/server/passkeys.rs rename to crates/tranquil-api/src/server/passkeys.rs index 15272b3..74a14e5 100644 --- a/crates/tranquil-pds/src/api/server/passkeys.rs +++ b/crates/tranquil-api/src/server/passkeys.rs @@ -1,7 +1,7 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window}; -use crate::state::AppState; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -271,6 +271,6 @@ pub async fn update_passkey( } } -pub async fn has_passkeys_for_user(state: &AppState, did: &crate::types::Did) -> bool { +pub async fn has_passkeys_for_user(state: &AppState, did: &tranquil_pds::types::Did) -> bool { state.user_repo.has_passkeys(did).await.unwrap_or(false) } diff --git a/crates/tranquil-pds/src/api/server/password.rs b/crates/tranquil-api/src/server/password.rs similarity index 94% rename from crates/tranquil-pds/src/api/server/password.rs rename to crates/tranquil-api/src/server/password.rs index 164de7f..92103ec 100644 --- a/crates/tranquil-pds/src/api/server/password.rs +++ b/crates/tranquil-api/src/server/password.rs @@ -1,13 +1,13 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; -use crate::auth::{ +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; +use tranquil_pds::auth::{ Active, Auth, NormalizedLoginIdentifier, require_legacy_session_mfa, require_reauth_window, require_reauth_window_if_available, }; -use crate::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit}; -use crate::state::AppState; -use crate::types::PlainPassword; -use crate::validation::validate_password; +use tranquil_pds::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::PlainPassword; +use tranquil_pds::validation::validate_password; use axum::{ Json, extract::State, @@ -19,7 +19,7 @@ use serde::Deserialize; use tracing::{error, info, warn}; fn generate_reset_code() -> String { - crate::util::generate_token_code() + tranquil_pds::util::generate_token_code() } #[derive(Deserialize)] @@ -78,7 +78,7 @@ pub async fn request_password_reset( return ApiError::InternalError(None).into_response(); } let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_password_reset( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_password_reset( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id, @@ -170,7 +170,7 @@ pub async fn reset_password( } }; futures::future::join_all(result.session_jtis.iter().map(|jti| { - let cache_key = crate::cache_keys::session_key(&result.did, jti); + let cache_key = tranquil_pds::cache_keys::session_key(&result.did, jti); let cache = state.cache.clone(); async move { if let Err(e) = cache.delete(&cache_key).await { @@ -184,7 +184,7 @@ pub async fn reset_password( .await; if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user_id).await { let actual_channel = - crate::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel); + tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel); if let Err(e) = state .user_repo .set_channel_verified(&user.did, actual_channel) @@ -212,7 +212,7 @@ pub async fn change_password( auth: Auth, Json(input): Json, ) -> Result { - use crate::auth::verify_password_mfa; + use tranquil_pds::auth::verify_password_mfa; let session_mfa = match require_legacy_session_mfa(&state, &auth).await { Ok(proof) => proof, diff --git a/crates/tranquil-pds/src/api/server/reauth.rs b/crates/tranquil-api/src/server/reauth.rs similarity index 93% rename from crates/tranquil-pds/src/api/server/reauth.rs rename to crates/tranquil-api/src/server/reauth.rs index e61d7b9..8f89e76 100644 --- a/crates/tranquil-pds/src/api/server/reauth.rs +++ b/crates/tranquil-api/src/server/reauth.rs @@ -1,4 +1,4 @@ -use crate::api::error::{ApiError, DbResultExt}; +use tranquil_pds::api::error::{ApiError, DbResultExt}; use axum::{ Json, extract::State, @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; use tracing::{error, info, warn}; use tranquil_db_traits::{SessionRepository, UserRepository, WebauthnChallengeType}; -use crate::auth::{Active, Auth}; -use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; -use crate::state::AppState; -use crate::types::PlainPassword; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::PlainPassword; pub const REAUTH_WINDOW_SECONDS: i64 = 300; @@ -125,7 +125,7 @@ pub async fn reauth_totp( .await?; let valid = - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) + crate::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) .await; if !valid { @@ -276,11 +276,11 @@ pub async fn reauth_passkey_finish( pub async fn update_last_reauth_cached( session_repo: &dyn SessionRepository, - cache: &std::sync::Arc, - did: &crate::types::Did, + cache: &std::sync::Arc, + did: &tranquil_pds::types::Did, ) -> Result, tranquil_db_traits::DbError> { let now = session_repo.update_last_reauth(did).await?; - let cache_key = crate::cache_keys::reauth_key(did); + let cache_key = tranquil_pds::cache_keys::reauth_key(did); let _ = cache .set( &cache_key, @@ -304,7 +304,7 @@ fn is_reauth_required(last_reauth_at: Option>) -> bool { async fn get_available_reauth_methods( user_repo: &dyn UserRepository, _session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> Vec { let mut methods = Vec::new(); @@ -334,7 +334,7 @@ async fn get_available_reauth_methods( pub async fn check_reauth_required( session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> bool { match session_repo.get_last_reauth_at(did).await { Ok(last_reauth_at) => is_reauth_required(last_reauth_at), @@ -344,10 +344,10 @@ pub async fn check_reauth_required( pub async fn check_reauth_required_cached( session_repo: &dyn SessionRepository, - cache: &std::sync::Arc, - did: &crate::types::Did, + cache: &std::sync::Arc, + did: &tranquil_pds::types::Did, ) -> bool { - let cache_key = crate::cache_keys::reauth_key(did); + let cache_key = tranquil_pds::cache_keys::reauth_key(did); if let Some(timestamp_str) = cache.get(&cache_key).await && let Ok(timestamp) = timestamp_str.parse::() { @@ -376,7 +376,7 @@ pub struct ReauthRequiredError { pub async fn reauth_required_response( user_repo: &dyn UserRepository, session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> Response { let methods = get_available_reauth_methods(user_repo, session_repo, did).await; ( @@ -392,7 +392,7 @@ pub async fn reauth_required_response( pub async fn check_legacy_session_mfa( session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> bool { match session_repo.get_session_mfa_status(did).await { Ok(Some(status)) => { @@ -416,7 +416,7 @@ pub async fn check_legacy_session_mfa( pub async fn update_mfa_verified( session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> Result<(), tranquil_db_traits::DbError> { session_repo.update_mfa_verified(did).await } @@ -424,7 +424,7 @@ pub async fn update_mfa_verified( pub async fn legacy_mfa_required_response( user_repo: &dyn UserRepository, session_repo: &dyn SessionRepository, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, ) -> Response { let methods = get_available_reauth_methods(user_repo, session_repo, did).await; ( diff --git a/crates/tranquil-pds/src/api/server/service_auth.rs b/crates/tranquil-api/src/server/service_auth.rs similarity index 93% rename from crates/tranquil-pds/src/api/server/service_auth.rs rename to crates/tranquil-api/src/server/service_auth.rs index 8c6c60d..28d5cf7 100644 --- a/crates/tranquil-pds/src/api/server/service_auth.rs +++ b/crates/tranquil-api/src/server/service_auth.rs @@ -1,7 +1,7 @@ -use crate::api::error::ApiError; -use crate::auth::extractor::{Auth, Permissive}; -use crate::state::AppState; -use crate::types::Did; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::extractor::{Auth, Permissive}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::Did; use axum::{ Json, extract::{Query, State}, @@ -75,7 +75,7 @@ pub async fn get_service_auth( match state.user_repo.get_user_info_by_did(&auth.did).await { Ok(Some(info)) => match info.key_bytes { Some(key_bytes_enc) => { - match crate::config::decrypt_key(&key_bytes_enc, info.encryption_version) { + match tranquil_pds::config::decrypt_key(&key_bytes_enc, info.encryption_version) { Ok(key) => key, Err(e) => { error!(error = ?e, "Failed to decrypt user key for service auth"); @@ -112,7 +112,7 @@ pub async fn get_service_auth( let lxm_for_token = lxm.map_or("*", |n| n.as_str()); if let Some(method) = lxm { - if let Err(e) = crate::auth::scope_check::check_rpc_scope( + if let Err(e) = tranquil_pds::auth::scope_check::check_rpc_scope( &auth.auth_source, auth.scope.as_deref(), params.aud.as_str(), @@ -167,7 +167,7 @@ pub async fn get_service_auth( } } - let service_token = match crate::auth::create_service_token( + let service_token = match tranquil_pds::auth::create_service_token( &auth.did, params.aud.as_str(), lxm_for_token, diff --git a/crates/tranquil-pds/src/api/server/session.rs b/crates/tranquil-api/src/server/session.rs similarity index 90% rename from crates/tranquil-pds/src/api/server/session.rs rename to crates/tranquil-api/src/server/session.rs index 612d454..f7f564b 100644 --- a/crates/tranquil-pds/src/api/server/session.rs +++ b/crates/tranquil-api/src/server/session.rs @@ -1,12 +1,12 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::api::{EmptyResponse, SuccessResponse}; -use crate::auth::{ +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::api::{EmptyResponse, SuccessResponse}; +use tranquil_pds::auth::{ Active, Auth, NormalizedLoginIdentifier, Permissive, require_legacy_session_mfa, require_reauth_window, }; -use crate::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit}; -use crate::state::AppState; -use crate::types::{AccountState, Did, Handle, PlainPassword}; +use tranquil_pds::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{AccountState, Did, Handle, PlainPassword}; use axum::{ Json, extract::State, @@ -93,7 +93,7 @@ pub async fn create_session( return ApiError::InternalError(None).into_response(); } }; - let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { + let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version) { Ok(k) => k, Err(e) => { error!("Failed to decrypt user key: {:?}", e); @@ -173,12 +173,12 @@ pub async fn create_session( let has_totp = row.totp_enabled; let email_2fa_enabled = row.email_2fa_enabled; let is_legacy_login = has_totp || email_2fa_enabled; - let twofa_ctx = crate::auth::legacy_2fa::Legacy2faContext { + let twofa_ctx = tranquil_pds::auth::legacy_2fa::Legacy2faContext { email_2fa_enabled, has_totp, allow_legacy_login: row.allow_legacy_login, }; - match crate::auth::legacy_2fa::process_legacy_2fa( + match tranquil_pds::auth::legacy_2fa::process_legacy_2fa( state.cache.as_ref(), &row.did, &twofa_ctx, @@ -186,14 +186,14 @@ pub async fn create_session( ) .await { - Ok(crate::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {} - Ok(crate::auth::legacy_2fa::Legacy2faOutcome::Blocked) => { + Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {} + Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Blocked) => { warn!("Legacy login blocked for TOTP-enabled account: {}", row.did); return ApiError::LegacyLoginBlocked.into_response(); } - Ok(crate::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => { + Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => { let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_2fa_code( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_2fa_code( state.user_repo.as_ref(), state.infra_repo.as_ref(), row.id, @@ -203,7 +203,7 @@ pub async fn create_session( .await { error!("Failed to send 2FA code: {:?}", e); - crate::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did).await; + tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did).await; return ApiError::InternalError(Some( "Failed to send verification code. Please try again.".into(), )) @@ -211,9 +211,9 @@ pub async fn create_session( } return ApiError::AuthFactorTokenRequired.into_response(); } - Ok(crate::auth::legacy_2fa::Legacy2faOutcome::Verified) => {} - Err(crate::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => { - use crate::auth::legacy_2fa::ChallengeError; + Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Verified) => {} + Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => { + use tranquil_pds::auth::legacy_2fa::ChallengeError; return match e { ChallengeError::CacheUnavailable => { error!("Cache unavailable for 2FA, blocking legacy login"); @@ -232,8 +232,8 @@ pub async fn create_session( } }; } - Err(crate::auth::legacy_2fa::Legacy2faFlowError::Validation(e)) => { - use crate::auth::legacy_2fa::ValidationError; + Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Validation(e)) => { + use tranquil_pds::auth::legacy_2fa::ValidationError; warn!("Invalid 2FA code for {}: {:?}", row.did, e); let msg = match e { ValidationError::TooManyAttempts => "Too many attempts. Please request a new code.", @@ -248,7 +248,7 @@ pub async fn create_session( return ApiError::InvalidCode(Some(msg.into())).into_response(); } } - let access_meta = match crate::auth::create_access_token_with_delegation( + let access_meta = match tranquil_pds::auth::create_access_token_with_delegation( &row.did, &key_bytes, app_password_scopes.as_deref(), @@ -261,7 +261,7 @@ pub async fn create_session( return ApiError::InternalError(None).into_response(); } }; - let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { + let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { Ok(m) => m, Err(e) => { error!("Failed to create refresh token: {:?}", e); @@ -297,7 +297,7 @@ pub async fn create_session( "Legacy login on TOTP-enabled account - sending notification" ); let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_legacy_login( state.user_repo.as_ref(), state.infra_repo.as_ref(), row.id, @@ -406,18 +406,18 @@ pub async fn delete_session( headers: axum::http::HeaderMap, _auth: Auth, ) -> Result { - let extracted = crate::auth::extract_auth_token_from_header(crate::util::get_header_str( + let extracted = tranquil_pds::auth::extract_auth_token_from_header(tranquil_pds::util::get_header_str( &headers, http::header::AUTHORIZATION, )) .ok_or(ApiError::AuthenticationRequired)?; - let jti = crate::auth::get_jti_from_token(&extracted.token) + let jti = tranquil_pds::auth::get_jti_from_token(&extracted.token) .map_err(|_| ApiError::AuthenticationFailed(None))?; - let did = crate::auth::get_did_from_token(&extracted.token).ok(); + let did = tranquil_pds::auth::get_did_from_token(&extracted.token).ok(); match state.session_repo.delete_session_by_access_jti(&jti).await { Ok(rows) if rows > 0 => { if let Some(did) = did { - let session_cache_key = crate::cache_keys::session_key(&did, &jti); + let session_cache_key = tranquil_pds::cache_keys::session_key(&did, &jti); let _ = state.cache.delete(&session_cache_key).await; } Ok(EmptyResponse::ok().into_response()) @@ -432,7 +432,7 @@ pub async fn refresh_session( _rate_limit: RateLimited, headers: axum::http::HeaderMap, ) -> Response { - let extracted = match crate::auth::extract_auth_token_from_header(crate::util::get_header_str( + let extracted = match tranquil_pds::auth::extract_auth_token_from_header(tranquil_pds::util::get_header_str( &headers, http::header::AUTHORIZATION, )) { @@ -440,7 +440,7 @@ pub async fn refresh_session( None => return ApiError::AuthenticationRequired.into_response(), }; let refresh_token = extracted.token; - let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) { + let refresh_jti = match tranquil_pds::auth::get_jti_from_token(&refresh_token) { Ok(jti) => jti, Err(_) => { return ApiError::AuthenticationFailed(Some("Invalid token format".into())) @@ -473,7 +473,7 @@ pub async fn refresh_session( return ApiError::InternalError(None).into_response(); } }; - let key_bytes = match crate::config::decrypt_key( + let key_bytes = match tranquil_pds::config::decrypt_key( &session_row.key_bytes, Some(session_row.encryption_version), ) { @@ -483,11 +483,11 @@ pub async fn refresh_session( return ApiError::InternalError(None).into_response(); } }; - if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { + if tranquil_pds::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())) .into_response(); } - let new_access_meta = match crate::auth::create_access_token_with_delegation( + let new_access_meta = match tranquil_pds::auth::create_access_token_with_delegation( &session_row.did, &key_bytes, session_row.scope.as_deref(), @@ -501,7 +501,7 @@ pub async fn refresh_session( } }; let new_refresh_meta = - match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) { + match tranquil_pds::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) { Ok(m) => m, Err(e) => { error!("Failed to create refresh token: {:?}", e); @@ -641,8 +641,8 @@ pub async fn confirm_signup( }; let normalized_token = - crate::auth::verification_token::normalize_token_input(&input.verification_code); - match crate::auth::verification_token::verify_signup_token( + tranquil_pds::auth::verification_token::normalize_token_input(&input.verification_code); + match tranquil_pds::auth::verification_token::verify_signup_token( &normalized_token, row.channel, &identifier, @@ -657,7 +657,7 @@ pub async fn confirm_signup( .into_response(); } } - Err(crate::auth::verification_token::VerifyError::Expired) => { + Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => { warn!("Verification code expired for user: {}", input.did); return ApiError::ExpiredToken(Some("Verification code has expired".into())) .into_response(); @@ -668,7 +668,7 @@ pub async fn confirm_signup( } } - let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { + let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version) { Ok(k) => k, Err(e) => { error!("Failed to decrypt user key: {:?}", e); @@ -676,14 +676,14 @@ pub async fn confirm_signup( } }; - let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { + let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&row.did, &key_bytes) { Ok(m) => m, Err(e) => { error!("Failed to create access token: {:?}", e); return ApiError::InternalError(None).into_response(); } }; - let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { + let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { Ok(m) => m, Err(e) => { error!("Failed to create refresh token: {:?}", e); @@ -718,7 +718,7 @@ pub async fn confirm_signup( } let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_welcome( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome( state.user_repo.as_ref(), state.infra_repo.as_ref(), row.id, @@ -749,7 +749,7 @@ pub struct AutoResendResult { } pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option { - let debounce_key = crate::cache_keys::auto_verify_sent_key(did.as_str()); + let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did.as_str()); let debounced = state.cache.get(&debounce_key).await.is_some(); let row = match state.user_repo.get_resend_verification_by_did(did).await { Ok(Some(row)) => row, @@ -789,11 +789,11 @@ pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option, ) -> Result { - let jti = crate::auth::extract_auth_token_from_header( + let jti = tranquil_pds::auth::extract_auth_token_from_header( headers.get("authorization").and_then(|v| v.to_str().ok()), ) - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) + .and_then(|extracted| tranquil_pds::auth::get_jti_from_token(&extracted.token).ok()) .ok_or(ApiError::InvalidToken(None))?; if auth.is_oauth() { @@ -1119,7 +1119,7 @@ pub async fn update_legacy_login_preference( .into_response()) } -use crate::comms::VALID_LOCALES; +use tranquil_pds::comms::VALID_LOCALES; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/tranquil-pds/src/api/server/signing_key.rs b/crates/tranquil-api/src/server/signing_key.rs similarity index 94% rename from crates/tranquil-pds/src/api/server/signing_key.rs rename to crates/tranquil-api/src/server/signing_key.rs index fa5921e..f6b377a 100644 --- a/crates/tranquil-pds/src/api/server/signing_key.rs +++ b/crates/tranquil-api/src/server/signing_key.rs @@ -1,5 +1,5 @@ -use crate::api::error::ApiError; -use crate::state::AppState; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -25,7 +25,7 @@ fn public_key_to_did_key(signing_key: &SigningKey) -> String { #[derive(Deserialize)] pub struct ReserveSigningKeyInput { - pub did: Option, + pub did: Option, } #[derive(Serialize)] diff --git a/crates/tranquil-pds/src/api/server/totp.rs b/crates/tranquil-api/src/server/totp.rs similarity index 94% rename from crates/tranquil-pds/src/api/server/totp.rs rename to crates/tranquil-api/src/server/totp.rs index 6359e9f..abbba4a 100644 --- a/crates/tranquil-pds/src/api/server/totp.rs +++ b/crates/tranquil-api/src/server/totp.rs @@ -1,14 +1,14 @@ -use crate::api::EmptyResponse; -use crate::api::error::{ApiError, DbResultExt}; -use crate::auth::{ +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::auth::{ Active, Auth, decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, require_legacy_session_mfa, verify_backup_code, verify_password_mfa, verify_totp_code, verify_totp_mfa, }; -use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; -use crate::state::AppState; -use crate::types::PlainPassword; +use tranquil_pds::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::PlainPassword; use axum::{ Json, extract::State, @@ -186,7 +186,7 @@ pub async fn disable_totp( .await .log_db_err("deleting TOTP")?; - crate::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &auth.did).await; + tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &auth.did).await; info!(did = %session_mfa.did(), "TOTP disabled (verified via {} and {})", password_mfa.method(), totp_mfa.method()); @@ -280,7 +280,7 @@ pub async fn regenerate_backup_codes( async fn verify_backup_code_for_user( state: &AppState, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, code: &str, ) -> bool { let code = code.trim().to_uppercase(); @@ -308,7 +308,7 @@ async fn verify_backup_code_for_user( pub async fn verify_totp_or_backup_for_user( state: &AppState, - did: &crate::types::Did, + did: &tranquil_pds::types::Did, code: &str, ) -> bool { use tranquil_db_traits::TotpRecordState; @@ -340,6 +340,6 @@ pub async fn verify_totp_or_backup_for_user( false } -pub async fn has_totp_enabled(state: &AppState, did: &crate::types::Did) -> bool { +pub async fn has_totp_enabled(state: &AppState, did: &tranquil_pds::types::Did) -> bool { state.user_repo.has_totp_enabled(did).await.unwrap_or(false) } diff --git a/crates/tranquil-pds/src/api/server/trusted_devices.rs b/crates/tranquil-api/src/server/trusted_devices.rs similarity index 97% rename from crates/tranquil-pds/src/api/server/trusted_devices.rs rename to crates/tranquil-api/src/server/trusted_devices.rs index e9e71ba..8a7a12a 100644 --- a/crates/tranquil-pds/src/api/server/trusted_devices.rs +++ b/crates/tranquil-api/src/server/trusted_devices.rs @@ -1,5 +1,5 @@ -use crate::api::SuccessResponse; -use crate::api::error::{ApiError, DbResultExt}; +use tranquil_pds::api::SuccessResponse; +use tranquil_pds::api::error::{ApiError, DbResultExt}; use axum::{ Json, extract::State, @@ -11,8 +11,8 @@ use tracing::{error, info}; use tranquil_db_traits::OAuthRepository; use tranquil_types::DeviceId; -use crate::auth::{Active, Auth}; -use crate::state::AppState; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::state::AppState; const TRUST_DURATION_DAYS: i64 = 30; diff --git a/crates/tranquil-pds/src/api/server/verify_email.rs b/crates/tranquil-api/src/server/verify_email.rs similarity index 85% rename from crates/tranquil-pds/src/api/server/verify_email.rs rename to crates/tranquil-api/src/server/verify_email.rs index 3003677..5b49a21 100644 --- a/crates/tranquil-pds/src/api/server/verify_email.rs +++ b/crates/tranquil-api/src/server/verify_email.rs @@ -1,10 +1,10 @@ -use crate::api::error::ApiError; -use crate::types::Did; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::types::Did; use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; -use crate::state::AppState; +use tranquil_pds::state::AppState; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -71,10 +71,10 @@ pub async fn resend_migration_verification( } let hostname = &tranquil_config::get().server.hostname; - let token = crate::auth::verification_token::generate_migration_token(&user.did, &email); - let formatted_token = crate::auth::verification_token::format_token_for_display(&token); + let token = tranquil_pds::auth::verification_token::generate_migration_token(&user.did, &email); + let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token); - if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), user.id, diff --git a/crates/tranquil-pds/src/api/server/verify_token.rs b/crates/tranquil-api/src/server/verify_token.rs similarity index 96% rename from crates/tranquil-pds/src/api/server/verify_token.rs rename to crates/tranquil-api/src/server/verify_token.rs index bf8aef3..27cc20e 100644 --- a/crates/tranquil-pds/src/api/server/verify_token.rs +++ b/crates/tranquil-api/src/server/verify_token.rs @@ -1,14 +1,14 @@ -use crate::api::error::{ApiError, DbResultExt}; -use crate::comms::comms_repo; -use crate::types::Did; +use tranquil_pds::api::error::{ApiError, DbResultExt}; +use tranquil_pds::comms::comms_repo; +use tranquil_pds::types::Did; use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; -use crate::auth::verification_token::{ +use tranquil_pds::auth::verification_token::{ VerificationPurpose, normalize_token_input, verify_token_signature, }; -use crate::state::AppState; +use tranquil_pds::state::AppState; use tranquil_db_traits::CommsChannel; #[derive(Deserialize, Clone)] @@ -46,7 +46,7 @@ pub async fn verify_token_internal( ApiError::from(e) })?; - let expected_hash = crate::auth::verification_token::hash_identifier(&identifier); + let expected_hash = tranquil_pds::auth::verification_token::hash_identifier(&identifier); if token_data.identifier_hash != expected_hash { return Err(ApiError::IdentifierMismatch); } diff --git a/crates/tranquil-pds/src/api/telegram_webhook.rs b/crates/tranquil-api/src/telegram_webhook.rs similarity index 98% rename from crates/tranquil-pds/src/api/telegram_webhook.rs rename to crates/tranquil-api/src/telegram_webhook.rs index 35f6620..3c09421 100644 --- a/crates/tranquil-pds/src/api/telegram_webhook.rs +++ b/crates/tranquil-api/src/telegram_webhook.rs @@ -6,8 +6,8 @@ use axum::{ use serde::Deserialize; use tracing::{debug, info, warn}; -use crate::comms::comms_repo; -use crate::state::AppState; +use tranquil_pds::comms::comms_repo; +use tranquil_pds::state::AppState; #[derive(Deserialize)] struct TelegramUpdate { diff --git a/crates/tranquil-pds/src/api/temp.rs b/crates/tranquil-api/src/temp.rs similarity index 94% rename from crates/tranquil-pds/src/api/temp.rs rename to crates/tranquil-api/src/temp.rs index 9887845..ef1a251 100644 --- a/crates/tranquil-pds/src/api/temp.rs +++ b/crates/tranquil-api/src/temp.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::auth::{Active, Auth, Permissive}; -use crate::state::AppState; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::{Active, Auth, Permissive}; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -57,7 +57,7 @@ pub async fn dereference_scope( for part in scope_parts { if let Some(cid_str) = part.strip_prefix("ref:") { - let cache_key = crate::cache_keys::scope_ref_key(cid_str); + let cache_key = tranquil_pds::cache_keys::scope_ref_key(cid_str); if let Some(cached) = state.cache.get(&cache_key).await { for s in cached.split_whitespace() { if !resolved_scopes.contains(&s.to_string()) { diff --git a/crates/tranquil-pds/src/api/verification.rs b/crates/tranquil-api/src/verification.rs similarity index 75% rename from crates/tranquil-pds/src/api/verification.rs rename to crates/tranquil-api/src/verification.rs index e460326..b2378cb 100644 --- a/crates/tranquil-pds/src/api/verification.rs +++ b/crates/tranquil-api/src/verification.rs @@ -1,5 +1,5 @@ -use crate::api::SuccessResponse; -use crate::state::AppState; +use tranquil_pds::api::SuccessResponse; +use tranquil_pds::state::AppState; use axum::{ Json, extract::State, @@ -19,12 +19,12 @@ pub async fn confirm_channel_verification( State(state): State, Json(input): Json, ) -> Response { - let token_input = crate::api::server::VerifyTokenInput { + let token_input = crate::server::VerifyTokenInput { token: input.code, identifier: input.identifier, }; - match crate::api::server::verify_token_internal(&state, token_input).await { + match crate::server::verify_token_internal(&state, token_input).await { Ok(_output) => SuccessResponse::ok().into_response(), Err(e) => e.into_response(), } diff --git a/crates/tranquil-oauth-server/Cargo.toml b/crates/tranquil-oauth-server/Cargo.toml new file mode 100644 index 0000000..f57cbe1 --- /dev/null +++ b/crates/tranquil-oauth-server/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "tranquil-oauth-server" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-pds = { workspace = true } +tranquil-api = { workspace = true } +tranquil-types = { workspace = true } +tranquil-config = { workspace = true } +tranquil-crypto = { workspace = true } +tranquil-db-traits = { workspace = true } + +axum = { workspace = true } +base64 = { workspace = true } +bcrypt = { workspace = true } +chrono = { workspace = true } +cid = { workspace = true } +hmac = { workspace = true } +http = { workspace = true } +jacquard-common = { workspace = true } +jacquard-repo = { workspace = true } +k256 = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true } +webauthn-rs = { workspace = true } diff --git a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs b/crates/tranquil-oauth-server/src/endpoints/authorize.rs similarity index 97% rename from crates/tranquil-pds/src/oauth/endpoints/authorize.rs rename to crates/tranquil-oauth-server/src/endpoints/authorize.rs index f00071c..5debe8d 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/authorize.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize.rs @@ -1,16 +1,16 @@ -use crate::auth::{BareLoginIdentifier, NormalizedLoginIdentifier}; -use crate::comms::comms_repo::enqueue_2fa_code; -use crate::oauth::{ +use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier}; +use tranquil_pds::comms::comms_repo::enqueue_2fa_code; +use tranquil_pds::oauth::{ AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId, db::should_show_consent, scopes::expand_include_scopes, }; -use crate::rate_limit::{ +use tranquil_pds::rate_limit::{ OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit, check_user_rate_limit, }; -use crate::state::AppState; -use crate::types::{Did, Handle, PlainPassword}; -use crate::util::extract_client_ip; +use tranquil_pds::state::AppState; +use tranquil_pds::types::{Did, Handle, PlainPassword}; +use tranquil_pds::util::extract_client_ip; use axum::{ Json, extract::{Query, State}, @@ -95,7 +95,7 @@ fn extract_device_cookie(headers: &HeaderMap) -> Option crate::api::server::has_passkeys_for_user(&state, &u.did).await, + Ok(Some(u)) => tranquil_api::server::has_passkeys_for_user(&state, &u.did).await, _ => false, }; @@ -2149,8 +2149,8 @@ pub async fn check_user_security_status( Option, ) = match user { Ok(Some(u)) => { - let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await; - let totp = crate::api::server::has_totp_enabled(&state, &u.did).await; + let passkeys = tranquil_api::server::has_passkeys_for_user(&state, &u.did).await; + let totp = tranquil_api::server::has_totp_enabled(&state, &u.did).await; let has_pw = u.password_hash.is_some(); let has_controllers = state .delegation_repo @@ -2297,7 +2297,7 @@ pub async fn passkey_start( let is_verified = user.channel_verification.has_any_verified(); if !is_verified { - let resend_info = crate::api::server::auto_resend_verification(&state, &user.did).await; + let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await; return ( StatusCode::FORBIDDEN, Json(serde_json::json!({ @@ -3125,14 +3125,14 @@ pub async fn authorize_passkey_finish( if has_totp { let device_cookie = extract_device_cookie(&headers); let device_is_trusted = if let Some(ref dev_id) = device_cookie { - crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await + tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await } else { false }; if device_is_trusted { if let Some(ref dev_id) = device_cookie { - let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) + let _ = tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) .await; } } else { @@ -3395,7 +3395,7 @@ pub async fn register_complete( }; if !is_verified { - let resend_info = crate::api::server::auto_resend_verification(&state, &did).await; + let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await; return ( StatusCode::FORBIDDEN, Json(serde_json::json!({ @@ -3503,7 +3503,7 @@ pub async fn register_complete( pub async fn establish_session( State(state): State, headers: HeaderMap, - auth: crate::auth::Auth, + auth: tranquil_pds::auth::Auth, ) -> Response { let did = &auth.did; diff --git a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs b/crates/tranquil-oauth-server/src/endpoints/delegation.rs similarity index 97% rename from crates/tranquil-pds/src/oauth/endpoints/delegation.rs rename to crates/tranquil-oauth-server/src/endpoints/delegation.rs index 84e9c23..7f4e07d 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/delegation.rs +++ b/crates/tranquil-oauth-server/src/endpoints/delegation.rs @@ -1,9 +1,9 @@ -use crate::auth::{Active, Auth}; -use crate::delegation::DelegationActionType; -use crate::rate_limit::{LoginLimit, OAuthRateLimited, TotpVerifyLimit}; -use crate::state::AppState; -use crate::types::PlainPassword; -use crate::util::extract_client_ip; +use tranquil_pds::auth::{Active, Auth}; +use tranquil_pds::delegation::DelegationActionType; +use tranquil_pds::rate_limit::{LoginLimit, OAuthRateLimited, TotpVerifyLimit}; +use tranquil_pds::state::AppState; +use tranquil_pds::types::PlainPassword; +use tranquil_pds::util::extract_client_ip; use axum::{ Json, extract::State, @@ -220,7 +220,7 @@ pub async fn delegation_auth( .into_response(); } - let has_totp = crate::api::server::has_totp_enabled(&state, &controller_did).await; + let has_totp = tranquil_api::server::has_totp_enabled(&state, &controller_did).await; if has_totp { return Json(DelegationAuthResponse { success: true, @@ -377,7 +377,7 @@ pub async fn delegation_totp_verify( }; let totp_valid = - crate::api::server::verify_totp_or_backup_for_user(&state, &controller_did, &form.code) + tranquil_api::server::verify_totp_or_backup_for_user(&state, &controller_did, &form.code) .await; if !totp_valid { return Json(DelegationAuthResponse { diff --git a/crates/tranquil-pds/src/oauth/endpoints/metadata.rs b/crates/tranquil-oauth-server/src/endpoints/metadata.rs similarity index 97% rename from crates/tranquil-pds/src/oauth/endpoints/metadata.rs rename to crates/tranquil-oauth-server/src/endpoints/metadata.rs index 1dbdf53..d441627 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/metadata.rs +++ b/crates/tranquil-oauth-server/src/endpoints/metadata.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; -use crate::oauth::jwks::{JwkSet, create_jwk_set}; -use crate::state::AppState; +use crate::jwks::{JwkSet, create_jwk_set}; +use tranquil_pds::state::AppState; use axum::{Json, extract::State}; use http::{HeaderName, header}; use serde::{Deserialize, Serialize}; @@ -129,8 +129,8 @@ pub async fn oauth_authorization_server( } pub async fn oauth_jwks(State(_state): State) -> Json { - use crate::config::AuthConfig; - use crate::oauth::jwks::Jwk; + use tranquil_pds::config::AuthConfig; + use crate::jwks::Jwk; let config = AuthConfig::get(); let server_key = Jwk { kty: "EC".to_string(), diff --git a/crates/tranquil-pds/src/oauth/endpoints/mod.rs b/crates/tranquil-oauth-server/src/endpoints/mod.rs similarity index 100% rename from crates/tranquil-pds/src/oauth/endpoints/mod.rs rename to crates/tranquil-oauth-server/src/endpoints/mod.rs diff --git a/crates/tranquil-pds/src/oauth/endpoints/par.rs b/crates/tranquil-oauth-server/src/endpoints/par.rs similarity index 97% rename from crates/tranquil-pds/src/oauth/endpoints/par.rs rename to crates/tranquil-oauth-server/src/endpoints/par.rs index 91af079..623175a 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/par.rs +++ b/crates/tranquil-oauth-server/src/endpoints/par.rs @@ -1,10 +1,10 @@ -use crate::oauth::{ +use tranquil_pds::oauth::{ AuthorizationRequestParameters, ClientAuth, ClientMetadataCache, CodeChallengeMethod, OAuthError, Prompt, RequestData, RequestId, ResponseMode, ResponseType, scopes::{ParsedScope, parse_scope}, }; -use crate::rate_limit::{OAuthParLimit, OAuthRateLimited}; -use crate::state::AppState; +use tranquil_pds::rate_limit::{OAuthParLimit, OAuthRateLimited}; +use tranquil_pds::state::AppState; use axum::body::Bytes; use axum::{Json, extract::State, http::HeaderMap}; use chrono::{Duration, Utc}; @@ -118,7 +118,7 @@ pub async fn pushed_authorization_request( .oauth_repo .create_authorization_request(&request_id_typed, &request_data) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; tokio::spawn({ let oauth_repo = state.oauth_repo.clone(); async move { @@ -159,7 +159,7 @@ fn determine_client_auth(request: &ParRequest) -> Result fn validate_scope( requested_scope: &Option, - client_metadata: &crate::oauth::ClientMetadata, + client_metadata: &tranquil_pds::oauth::ClientMetadata, ) -> Result, OAuthError> { let scope_str = match requested_scope { Some(s) if !s.is_empty() => s, diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs similarity index 96% rename from crates/tranquil-pds/src/oauth/endpoints/token/grants.rs rename to crates/tranquil-oauth-server/src/endpoints/token/grants.rs index c817992..21e12a9 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/grants.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs @@ -2,16 +2,16 @@ use super::helpers::{create_access_token_with_delegation, verify_pkce}; use super::types::{ RequestClientAuth, TokenGrant, TokenResponse, TokenType, ValidatedTokenRequest, }; -use crate::config::AuthConfig; -use crate::delegation::intersect_scopes; -use crate::oauth::{ +use tranquil_pds::config::AuthConfig; +use tranquil_pds::delegation::intersect_scopes; +use tranquil_pds::oauth::{ AuthFlow, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, TokenData, TokenId, db::{enforce_token_limit_for_user, lookup_refresh_token}, scopes::expand_include_scopes, verify_client_auth, }; -use crate::state::AppState; +use tranquil_pds::state::AppState; use axum::Json; use axum::http::{HeaderMap, Method}; use chrono::{Duration, Utc}; @@ -50,7 +50,7 @@ pub async fn handle_authorization_code_grant( .oauth_repo .consume_authorization_request_by_code(&auth_code) .await - .map_err(crate::oauth::db_err_to_oauth)? + .map_err(tranquil_pds::oauth::db_err_to_oauth)? .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?; let flow = AuthFlow::from_request_data(auth_request) @@ -107,7 +107,7 @@ pub async fn handle_authorization_code_grant( .oauth_repo .check_and_record_dpop_jti(&result.jti) .await - .map_err(crate::oauth::db_err_to_oauth)? + .map_err(tranquil_pds::oauth::db_err_to_oauth)? { return Err(OAuthError::InvalidDpopProof( "DPoP proof has already been used".to_string(), @@ -201,7 +201,7 @@ pub async fn handle_authorization_code_grant( .oauth_repo .create_token(&token_data) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; tracing::info!( did = %did, token_id = %token_id.0, @@ -321,7 +321,7 @@ pub async fn handle_refresh_token_grant( .oauth_repo .delete_token_family(original_token_id) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; return Err(OAuthError::InvalidGrant( "Refresh token reuse detected, token family revoked".to_string(), )); @@ -332,7 +332,7 @@ pub async fn handle_refresh_token_grant( .oauth_repo .delete_token_family(db_id) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; return Err(OAuthError::InvalidGrant( "Refresh token has expired".to_string(), )); @@ -354,7 +354,7 @@ pub async fn handle_refresh_token_grant( .oauth_repo .check_and_record_dpop_jti(&result.jti) .await - .map_err(crate::oauth::db_err_to_oauth)? + .map_err(tranquil_pds::oauth::db_err_to_oauth)? { return Err(OAuthError::InvalidDpopProof( "DPoP proof has already been used".to_string(), @@ -387,7 +387,7 @@ pub async fn handle_refresh_token_grant( .oauth_repo .rotate_token(db_id, &new_refresh_typed, new_expires_at) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; tracing::info!( did = %token_data.did, new_expires_at = %new_expires_at, diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs similarity index 98% rename from crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs rename to crates/tranquil-oauth-server/src/endpoints/token/helpers.rs index 62d7a72..e80fb63 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs @@ -1,5 +1,5 @@ -use crate::config::AuthConfig; -use crate::oauth::OAuthError; +use tranquil_pds::config::AuthConfig; +use tranquil_pds::oauth::OAuthError; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use chrono::Utc; diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs b/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs similarity index 92% rename from crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs rename to crates/tranquil-oauth-server/src/endpoints/token/introspect.rs index 5987b0c..e0a8d2c 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs @@ -1,7 +1,7 @@ use super::helpers::extract_token_claims; -use crate::oauth::OAuthError; -use crate::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited}; -use crate::state::AppState; +use tranquil_pds::oauth::OAuthError; +use tranquil_pds::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited}; +use tranquil_pds::state::AppState; use axum::extract::State; use axum::http::StatusCode; use axum::{Form, Json}; @@ -27,20 +27,20 @@ pub async fn revoke_token( .oauth_repo .get_token_by_refresh_token(&refresh_token) .await - .map_err(crate::oauth::db_err_to_oauth)? + .map_err(tranquil_pds::oauth::db_err_to_oauth)? { state .oauth_repo .delete_token_family(db_id) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; } else { let token_id = TokenId::from(token.clone()); state .oauth_repo .delete_token(&token_id) .await - .map_err(crate::oauth::db_err_to_oauth)?; + .map_err(tranquil_pds::oauth::db_err_to_oauth)?; } } Ok(StatusCode::OK) diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/mod.rs b/crates/tranquil-oauth-server/src/endpoints/token/mod.rs similarity index 91% rename from crates/tranquil-pds/src/oauth/endpoints/token/mod.rs rename to crates/tranquil-oauth-server/src/endpoints/token/mod.rs index f0b3c1c..24c8cd1 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/mod.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/mod.rs @@ -3,9 +3,9 @@ mod helpers; mod introspect; mod types; -use crate::oauth::OAuthError; -use crate::rate_limit::{OAuthRateLimited, OAuthTokenLimit}; -use crate::state::AppState; +use tranquil_pds::oauth::OAuthError; +use tranquil_pds::rate_limit::{OAuthRateLimited, OAuthTokenLimit}; +use tranquil_pds::state::AppState; use axum::body::Bytes; use axum::{Json, extract::State, http::HeaderMap}; @@ -41,7 +41,7 @@ pub async fn token_endpoint( )); }; let dpop_proof = headers - .get(crate::util::HEADER_DPOP) + .get(tranquil_pds::util::HEADER_DPOP) .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); let validated = request.validate()?; diff --git a/crates/tranquil-pds/src/oauth/endpoints/token/types.rs b/crates/tranquil-oauth-server/src/endpoints/token/types.rs similarity index 99% rename from crates/tranquil-pds/src/oauth/endpoints/token/types.rs rename to crates/tranquil-oauth-server/src/endpoints/token/types.rs index b7f1a2b..d070a2d 100644 --- a/crates/tranquil-pds/src/oauth/endpoints/token/types.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/types.rs @@ -1,4 +1,4 @@ -use crate::oauth::OAuthError; +use tranquil_pds::oauth::OAuthError; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/tranquil-pds/src/oauth/jwks.rs b/crates/tranquil-oauth-server/src/jwks.rs similarity index 100% rename from crates/tranquil-pds/src/oauth/jwks.rs rename to crates/tranquil-oauth-server/src/jwks.rs diff --git a/crates/tranquil-oauth-server/src/lib.rs b/crates/tranquil-oauth-server/src/lib.rs new file mode 100644 index 0000000..b7b52b4 --- /dev/null +++ b/crates/tranquil-oauth-server/src/lib.rs @@ -0,0 +1,116 @@ +pub mod endpoints; +pub mod jwks; +pub mod sso_endpoints; + +use tranquil_pds::state::AppState; + +pub fn oauth_routes() -> axum::Router { + use axum::{middleware, routing::{get, post}}; + + axum::Router::new() + .route("/jwks", get(endpoints::oauth_jwks)) + .route("/par", post(endpoints::pushed_authorization_request)) + .route("/authorize", get(endpoints::authorize_get)) + .route("/authorize", post(endpoints::authorize_post)) + .route( + "/authorize/accounts", + get(endpoints::authorize_accounts), + ) + .route( + "/authorize/select", + post(endpoints::authorize_select), + ) + .route("/authorize/2fa", get(endpoints::authorize_2fa_get)) + .route("/authorize/2fa", post(endpoints::authorize_2fa_post)) + .route( + "/authorize/passkey", + get(endpoints::authorize_passkey_start), + ) + .route( + "/authorize/passkey", + post(endpoints::authorize_passkey_finish), + ) + .route( + "/passkey/check", + get(endpoints::check_user_has_passkeys), + ) + .route( + "/security-status", + get(endpoints::check_user_security_status), + ) + .route("/passkey/start", post(endpoints::passkey_start)) + .route("/passkey/finish", post(endpoints::passkey_finish)) + .route("/authorize/deny", post(endpoints::authorize_deny)) + .route( + "/register/complete", + post(endpoints::register_complete), + ) + .route( + "/establish-session", + post(endpoints::establish_session), + ) + .route("/authorize/consent", get(endpoints::consent_get)) + .route("/authorize/consent", post(endpoints::consent_post)) + .route("/authorize/renew", post(endpoints::authorize_renew)) + .route( + "/authorize/redirect", + get(endpoints::authorize_redirect), + ) + .route("/delegation/auth", post(endpoints::delegation_auth)) + .route( + "/delegation/auth-token", + post(endpoints::delegation_auth_token), + ) + .route( + "/delegation/totp", + post(endpoints::delegation_totp_verify), + ) + .route("/token", post(endpoints::token_endpoint)) + .route("/revoke", post(endpoints::revoke_token)) + .route("/introspect", post(endpoints::introspect_token)) + .route("/sso/providers", get(sso_endpoints::get_sso_providers)) + .route("/sso/initiate", post(sso_endpoints::sso_initiate)) + .route( + "/sso/callback", + get(sso_endpoints::sso_callback).post(sso_endpoints::sso_callback_post), + ) + .route("/sso/linked", get(sso_endpoints::get_linked_accounts)) + .route("/sso/unlink", post(sso_endpoints::unlink_account)) + .route( + "/sso/pending-registration", + get(sso_endpoints::get_pending_registration), + ) + .route( + "/sso/complete-registration", + post(sso_endpoints::complete_registration), + ) + .route( + "/sso/check-handle-available", + get(sso_endpoints::check_handle_available), + ) + .layer(middleware::from_fn(tranquil_pds::oauth::verify::dpop_nonce_middleware)) +} + +pub fn well_known_oauth_routes() -> axum::Router { + use axum::routing::get; + + axum::Router::new() + .route( + "/oauth-protected-resource", + get(endpoints::oauth_protected_resource), + ) + .route( + "/oauth-authorization-server", + get(endpoints::oauth_authorization_server), + ) +} + +pub fn frontend_client_metadata_route() -> axum::Router { + use axum::routing::get; + + axum::Router::new() + .route( + "/oauth-client-metadata.json", + get(endpoints::frontend_client_metadata), + ) +} diff --git a/crates/tranquil-pds/src/sso/endpoints.rs b/crates/tranquil-oauth-server/src/sso_endpoints.rs similarity index 93% rename from crates/tranquil-pds/src/sso/endpoints.rs rename to crates/tranquil-oauth-server/src/sso_endpoints.rs index 5159660..4b1d3ef 100644 --- a/crates/tranquil-pds/src/sso/endpoints.rs +++ b/crates/tranquil-oauth-server/src/sso_endpoints.rs @@ -9,15 +9,15 @@ use serde::{Deserialize, Serialize}; use tranquil_db_traits::{SsoAction, SsoProviderType}; use tranquil_types::RequestId; -use super::config::SsoConfig; -use crate::api::error::ApiError; -use crate::auth::extractor::extract_auth_token_from_header; -use crate::auth::{generate_app_password, validate_bearer_token_cached}; -use crate::rate_limit::{ +use tranquil_pds::sso::SsoConfig; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::auth::extractor::extract_auth_token_from_header; +use tranquil_pds::auth::{generate_app_password, validate_bearer_token_cached}; +use tranquil_pds::rate_limit::{ AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit, check_user_rate_limit_with_message, }; -use crate::state::AppState; +use tranquil_pds::state::AppState; fn generate_state() -> String { use rand::RngCore; @@ -367,7 +367,7 @@ async fn handle_sso_login( state: &AppState, request_uri: &str, provider: SsoProviderType, - user_info: &crate::sso::providers::SsoUserInfo, + user_info: &tranquil_pds::sso::providers::SsoUserInfo, ) -> Response { let identity = match state .sso_repo @@ -482,7 +482,7 @@ async fn handle_sso_link( state: &AppState, did: tranquil_types::Did, provider: SsoProviderType, - user_info: &crate::sso::providers::SsoUserInfo, + user_info: &tranquil_pds::sso::providers::SsoUserInfo, ) -> Response { let existing = state .sso_repo @@ -555,7 +555,7 @@ async fn handle_sso_register( state: &AppState, request_uri: &str, provider: SsoProviderType, - user_info: &crate::sso::providers::SsoUserInfo, + user_info: &tranquil_pds::sso::providers::SsoUserInfo, ) -> Response { match state .sso_repo @@ -616,7 +616,7 @@ pub struct LinkedAccountsResponse { pub async fn get_linked_accounts( State(state): State, - auth: crate::auth::Auth, + auth: tranquil_pds::auth::Auth, ) -> Result, ApiError> { let identities = state .sso_repo @@ -651,7 +651,7 @@ pub struct UnlinkAccountResponse { pub async fn unlink_account( State(state): State, - auth: crate::auth::Auth, + auth: tranquil_pds::auth::Auth, Json(input): Json, ) -> Result, ApiError> { let _rate_limit = check_user_rate_limit_with_message::( @@ -763,7 +763,7 @@ pub async fn check_handle_available( })); } - let validated = match crate::api::validation::validate_short_handle(&query.handle) { + let validated = match tranquil_pds::api::validation::validate_short_handle(&query.handle) { Ok(h) => h, Err(e) => { return Ok(Json(CheckHandleResponse { @@ -781,7 +781,7 @@ pub async fn check_handle_available( } let domain = query.domain.as_deref().unwrap_or(&available_domains[0]); let full_handle = format!("{}.{}", validated, domain); - let handle_typed: crate::types::Handle = match full_handle.parse() { + let handle_typed: tranquil_pds::types::Handle = match full_handle.parse() { Ok(h) => h, Err(_) => return Err(ApiError::InvalidHandle(None)), }; @@ -879,12 +879,12 @@ pub async fn complete_registration( .unwrap_or(&input.handle), None => &input.handle, }; - match crate::api::validation::validate_short_handle(handle_to_validate) { + match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])), Err(_) => return Err(ApiError::InvalidHandle(None)), } } else { - match crate::api::validation::validate_full_domain_handle(&input.handle) { + match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { Ok(h) => h, Err(_) => return Err(ApiError::InvalidHandle(None)), } @@ -914,7 +914,7 @@ pub async fn complete_registration( tranquil_db_traits::CommsChannel::Discord => match &input.discord_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().to_lowercase(); - if !crate::api::validation::is_valid_discord_username(&clean) { + if !tranquil_pds::api::validation::is_valid_discord_username(&clean) { return Err(ApiError::InvalidRequest( "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(), )); @@ -926,7 +926,7 @@ pub async fn complete_registration( tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username { Some(username) if !username.trim().is_empty() => { let clean = username.trim().trim_start_matches('@'); - if !crate::api::validation::is_valid_telegram_username(clean) { + if !tranquil_pds::api::validation::is_valid_telegram_username(clean) { return Err(ApiError::InvalidRequest( "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(), )); @@ -960,7 +960,7 @@ pub async fn complete_registration( if e.len() > 254 { return Err(ApiError::InvalidEmail); } - if !crate::api::validation::is_valid_email(e) { + if !tranquil_pds::api::validation::is_valid_email(e) { return Err(ApiError::InvalidEmail); } Some(e.clone()) @@ -981,7 +981,7 @@ pub async fn complete_registration( None }; - let handle_typed: crate::types::Handle = + let handle_typed: tranquil_pds::types::Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?; let reserved = state .user_repo @@ -1008,7 +1008,7 @@ pub async fn complete_registration( let did = match did_type { "web" => { - if !crate::api::server::meta::is_self_hosted_did_web_enabled() { + if !tranquil_pds::util::is_self_hosted_did_web_enabled() { return Err(ApiError::SelfHostedDidWebDisabled); } let encoded_handle = handle.replace(':', "%3A"); @@ -1038,9 +1038,9 @@ pub async fn complete_registration( .secrets .plc_rotation_key .clone() - .unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key)); + .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key)); - let genesis_result = match crate::plc::create_genesis_operation( + let genesis_result = match tranquil_pds::plc::create_genesis_operation( &signing_key, &rotation_key, &handle, @@ -1055,7 +1055,7 @@ pub async fn complete_registration( } }; - let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone())); + let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone())); if let Err(e) = plc_client .send_operation(&genesis_result.did, &genesis_result.signed_operation) .await @@ -1071,7 +1071,7 @@ pub async fn complete_registration( }; tracing::info!(did = %did, handle = %handle, provider = %pending_preview.provider.as_str(), "Created DID for SSO account"); - let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { + let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) { Ok(bytes) => bytes, Err(e) => { tracing::error!("Error encrypting signing key: {:?}", e); @@ -1089,10 +1089,10 @@ pub async fn complete_registration( }; let rev = Tid::now(LimitedU32::MIN); - let did_typed: crate::types::Did = did + let did_typed: tranquil_pds::types::Did = did .parse() .map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?; - let (commit_bytes, _sig) = match crate::api::repo::record::utils::create_signed_commit( + let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit( &did_typed, mst_root, rev.as_ref(), @@ -1146,7 +1146,7 @@ pub async fn complete_registration( .map(|s| s.trim().trim_start_matches('@').to_lowercase()) .filter(|s| !s.is_empty()), encrypted_key_bytes: encrypted_key_bytes.clone(), - encryption_version: crate::config::ENCRYPTION_VERSION, + encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, commit_cid: commit_cid.to_string(), repo_rev: rev.as_ref().to_string(), genesis_block_cids, @@ -1189,12 +1189,12 @@ pub async fn complete_registration( .await; if let Err(e) = - crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed)) + tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed)) .await { tracing::warn!("Failed to sequence identity event for {}: {}", did, e); } - if let Err(e) = crate::api::repo::record::sequence_account_event( + if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( &state, &did_typed, tranquil_db_traits::AccountStatus::Active, @@ -1208,11 +1208,11 @@ pub async fn complete_registration( "$type": "app.bsky.actor.profile", "displayName": handle_typed.as_str() }); - if let Err(e) = crate::api::repo::record::create_record_internal( + if let Err(e) = tranquil_pds::repo_ops::create_record_internal( &state, &did_typed, - &crate::types::PROFILE_COLLECTION, - &crate::types::PROFILE_RKEY, + &tranquil_pds::types::PROFILE_COLLECTION, + &tranquil_pds::types::PROFILE_RKEY, &profile_record, ) .await @@ -1287,9 +1287,9 @@ pub async fn complete_registration( tracing::info!(did = %did, "Auto-verified email from SSO provider"); if is_standalone { - let key_bytes = match crate::config::decrypt_key( + let key_bytes = match tranquil_pds::config::decrypt_key( &encrypted_key_bytes, - Some(crate::config::ENCRYPTION_VERSION), + Some(tranquil_pds::config::ENCRYPTION_VERSION), ) { Ok(k) => k, Err(e) => { @@ -1298,7 +1298,7 @@ pub async fn complete_registration( } }; - let access_meta = match crate::auth::create_access_token_with_metadata(&did, &key_bytes) + let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&did, &key_bytes) { Ok(m) => m, Err(e) => { @@ -1307,7 +1307,7 @@ pub async fn complete_registration( } }; let refresh_meta = - match crate::auth::create_refresh_token_with_metadata(&did, &key_bytes) { + match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &key_bytes) { Ok(m) => m, Err(e) => { tracing::error!("Failed to create refresh token: {:?}", e); @@ -1333,7 +1333,7 @@ pub async fn complete_registration( } let hostname = &tranquil_config::get().server.hostname; - if let Err(e) = crate::comms::comms_repo::enqueue_welcome( + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome( state.user_repo.as_ref(), state.infra_repo.as_ref(), user_id.unwrap_or(uuid::Uuid::nil()), @@ -1370,14 +1370,14 @@ pub async fn complete_registration( } if let Some(uid) = user_id { - let verification_token = crate::auth::verification_token::generate_signup_token( + let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( &did_typed, verification_channel, &verification_recipient, ); let formatted_token = - crate::auth::verification_token::format_token_for_display(&verification_token); - if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( + tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); + if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( state.user_repo.as_ref(), state.infra_repo.as_ref(), uid, diff --git a/crates/tranquil-pds/Cargo.toml b/crates/tranquil-pds/Cargo.toml index 1b05b4c..cc04f20 100644 --- a/crates/tranquil-pds/Cargo.toml +++ b/crates/tranquil-pds/Cargo.toml @@ -31,8 +31,6 @@ bs58 = { workspace = true } bytes = { workspace = true } chrono = { workspace = true } cid = { workspace = true } -clap = { workspace = true } -dotenvy = { workspace = true } ed25519-dalek = { workspace = true } futures = { workspace = true } hex = { workspace = true } @@ -75,7 +73,6 @@ tower = { workspace = true } tower-http = { workspace = true } tower-layer = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } urlencoding = { workspace = true } uuid = { workspace = true } webauthn-rs = { workspace = true } @@ -97,4 +94,7 @@ ctor = { workspace = true } testcontainers = { workspace = true } testcontainers-modules = { workspace = true } tranquil-ripple = { workspace = true } +tranquil-sync = { workspace = true } +tranquil-api = { workspace = true } +tranquil-oauth-server = { workspace = true } wiremock = { workspace = true } diff --git a/crates/tranquil-pds/src/api/mod.rs b/crates/tranquil-pds/src/api/mod.rs index 4d33840..6964179 100644 --- a/crates/tranquil-pds/src/api/mod.rs +++ b/crates/tranquil-pds/src/api/mod.rs @@ -1,22 +1,8 @@ -pub mod actor; -pub mod admin; -pub mod age_assurance; -pub mod backup; -pub mod delegation; -pub mod discord_webhook; pub mod error; -pub mod identity; -pub mod moderation; -pub mod notification_prefs; pub mod proxy; pub mod proxy_client; -pub mod repo; pub mod responses; -pub mod server; -pub mod telegram_webhook; -pub mod temp; pub mod validation; -pub mod verification; pub use error::ApiError; pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_limit}; diff --git a/crates/tranquil-pds/src/auth/mfa_verified.rs b/crates/tranquil-pds/src/auth/mfa_verified.rs index ec861c9..8d0ee1e 100644 --- a/crates/tranquil-pds/src/auth/mfa_verified.rs +++ b/crates/tranquil-pds/src/auth/mfa_verified.rs @@ -74,7 +74,7 @@ pub async fn require_legacy_session_mfa<'a>( state: &AppState, user: &'a AuthenticatedUser, ) -> Result, Response> { - use crate::api::server::reauth::{check_legacy_session_mfa, legacy_mfa_required_response}; + use crate::auth::reauth::{check_legacy_session_mfa, legacy_mfa_required_response}; if check_legacy_session_mfa(&*state.session_repo, &user.did).await { Ok(MfaVerified::from_session_reauth(user)) @@ -87,7 +87,7 @@ pub async fn require_reauth_window<'a>( state: &AppState, user: &'a AuthenticatedUser, ) -> Result, Response> { - use crate::api::server::reauth::{REAUTH_WINDOW_SECONDS, reauth_required_response}; + use crate::auth::reauth::{REAUTH_WINDOW_SECONDS, reauth_required_response}; use chrono::Utc; let status = state @@ -117,7 +117,7 @@ pub async fn require_reauth_window_if_available<'a>( state: &AppState, user: &'a AuthenticatedUser, ) -> Result>, Response> { - use crate::api::server::reauth::{check_reauth_required_cached, reauth_required_response}; + use crate::auth::reauth::{check_reauth_required_cached, reauth_required_response}; let has_password = state .user_repo diff --git a/crates/tranquil-pds/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs index 9a2f638..e517578 100644 --- a/crates/tranquil-pds/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -13,6 +13,7 @@ use tranquil_db_traits::OAuthRepository; pub mod account_verified; pub mod email_token; pub mod extractor; +pub mod reauth; pub mod legacy_2fa; pub mod login_identifier; pub mod mfa_verified; diff --git a/crates/tranquil-pds/src/auth/reauth.rs b/crates/tranquil-pds/src/auth/reauth.rs new file mode 100644 index 0000000..a581ff4 --- /dev/null +++ b/crates/tranquil-pds/src/auth/reauth.rs @@ -0,0 +1,156 @@ +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use chrono::Utc; +use serde::Serialize; +use tranquil_db_traits::{SessionRepository, UserRepository}; + +pub const REAUTH_WINDOW_SECONDS: i64 = 300; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ReauthMethod { + Password, + Totp, + Passkey, +} + +fn is_reauth_required(last_reauth_at: Option>) -> bool { + match last_reauth_at { + None => true, + Some(t) => { + let elapsed = Utc::now().signed_duration_since(t); + elapsed.num_seconds() > REAUTH_WINDOW_SECONDS + } + } +} + +async fn get_available_reauth_methods( + user_repo: &dyn UserRepository, + _session_repo: &dyn SessionRepository, + did: &crate::types::Did, +) -> Vec { + let mut methods = Vec::new(); + + let has_password = user_repo + .get_password_hash_by_did(did) + .await + .ok() + .flatten() + .is_some(); + + if has_password { + methods.push(ReauthMethod::Password); + } + + let has_totp = user_repo.has_totp_enabled(did).await.unwrap_or(false); + if has_totp { + methods.push(ReauthMethod::Totp); + } + + let has_passkeys = user_repo.has_passkeys(did).await.unwrap_or(false); + if has_passkeys { + methods.push(ReauthMethod::Passkey); + } + + methods +} + +pub async fn check_reauth_required_cached( + session_repo: &dyn SessionRepository, + cache: &std::sync::Arc, + did: &crate::types::Did, +) -> bool { + let cache_key = crate::cache_keys::reauth_key(did); + if let Some(timestamp_str) = cache.get(&cache_key).await + && let Ok(timestamp) = timestamp_str.parse::() + { + let reauth_time = chrono::DateTime::from_timestamp(timestamp, 0); + if let Some(t) = reauth_time { + let elapsed = Utc::now().signed_duration_since(t); + if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS { + return false; + } + } + } + match session_repo.get_last_reauth_at(did).await { + Ok(last_reauth_at) => is_reauth_required(last_reauth_at), + _ => true, + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReauthRequiredError { + pub error: String, + pub message: String, + pub reauth_methods: Vec, +} + +pub async fn reauth_required_response( + user_repo: &dyn UserRepository, + session_repo: &dyn SessionRepository, + did: &crate::types::Did, +) -> Response { + let methods = get_available_reauth_methods(user_repo, session_repo, did).await; + ( + StatusCode::UNAUTHORIZED, + Json(ReauthRequiredError { + error: "ReauthRequired".to_string(), + message: "Re-authentication required for this action".to_string(), + reauth_methods: methods, + }), + ) + .into_response() +} + +pub async fn check_legacy_session_mfa( + session_repo: &dyn SessionRepository, + did: &crate::types::Did, +) -> bool { + match session_repo.get_session_mfa_status(did).await { + Ok(Some(status)) => { + if status.login_type.is_modern() { + return true; + } + if status.mfa_verified { + return true; + } + if let Some(last_reauth) = status.last_reauth_at { + let elapsed = chrono::Utc::now().signed_duration_since(last_reauth); + if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS { + return true; + } + } + false + } + _ => true, + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MfaVerificationRequiredError { + pub error: String, + pub message: String, + pub reauth_methods: Vec, +} + +pub async fn legacy_mfa_required_response( + user_repo: &dyn UserRepository, + session_repo: &dyn SessionRepository, + did: &crate::types::Did, +) -> Response { + let methods = get_available_reauth_methods(user_repo, session_repo, did).await; + ( + StatusCode::FORBIDDEN, + Json(MfaVerificationRequiredError { + error: "MfaVerificationRequired".to_string(), + message: "This sensitive operation requires MFA verification. Your session was created via a legacy app that doesn't support MFA during login.".to_string(), + reauth_methods: methods, + }), + ) + .into_response() +} diff --git a/crates/tranquil-pds/src/crawlers.rs b/crates/tranquil-pds/src/crawlers.rs index 40f7205..deff0ca 100644 --- a/crates/tranquil-pds/src/crawlers.rs +++ b/crates/tranquil-pds/src/crawlers.rs @@ -1,5 +1,5 @@ use crate::circuit_breaker::CircuitBreaker; -use crate::sync::firehose::SequencedEvent; +use tranquil_db_traits::SequencedEvent; use reqwest::Client; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/crates/tranquil-pds/src/lib.rs b/crates/tranquil-pds/src/lib.rs index 0d00bae..0962071 100644 --- a/crates/tranquil-pds/src/lib.rs +++ b/crates/tranquil-pds/src/lib.rs @@ -17,6 +17,7 @@ pub mod oauth; pub mod plc; pub mod rate_limit; pub mod repo; +pub mod repo_ops; pub mod repo_write_lock; pub mod scheduled; pub mod sso; @@ -33,7 +34,7 @@ use axum::{ extract::DefaultBodyLimit, http::Method, middleware, - routing::{get, post}, + routing::get, }; use http::StatusCode; use serde_json::json; @@ -56,496 +57,30 @@ pub const BUILD_VERSION: &str = concat!( #[cfg(not(debug_assertions))] pub const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub struct ExternalRoutes { + pub xrpc: Router, + pub oauth: Router, + pub well_known: Router, + pub extra: Router, +} + +impl Default for ExternalRoutes { + fn default() -> Self { + Self { + xrpc: Router::new(), + oauth: Router::new(), + well_known: Router::new(), + extra: Router::new(), + } + } +} + pub fn app(state: AppState) -> Router { - let xrpc_router = Router::new() - .route("/_health", get(api::server::health)) - .route( - "/com.atproto.server.describeServer", - get(api::server::describe_server), - ) - .route( - "/com.atproto.server.createAccount", - post(api::identity::create_account), - ) - .route( - "/com.atproto.server.createSession", - post(api::server::create_session), - ) - .route( - "/com.atproto.server.getSession", - get(api::server::get_session), - ) - .route("/_account.listSessions", get(api::server::list_sessions)) - .route("/_account.revokeSession", post(api::server::revoke_session)) - .route( - "/_account.revokeAllSessions", - post(api::server::revoke_all_sessions), - ) - .route( - "/com.atproto.server.deleteSession", - post(api::server::delete_session), - ) - .route( - "/com.atproto.server.refreshSession", - post(api::server::refresh_session), - ) - .route( - "/com.atproto.server.confirmSignup", - post(api::server::confirm_signup), - ) - .route( - "/com.atproto.server.resendVerification", - post(api::server::resend_verification), - ) - .route( - "/com.atproto.server.getServiceAuth", - get(api::server::get_service_auth), - ) - .route( - "/com.atproto.identity.resolveHandle", - get(api::identity::resolve_handle), - ) - .route( - "/com.atproto.repo.createRecord", - post(api::repo::create_record), - ) - .route("/com.atproto.repo.putRecord", post(api::repo::put_record)) - .route("/com.atproto.repo.getRecord", get(api::repo::get_record)) - .route( - "/com.atproto.repo.deleteRecord", - post(api::repo::delete_record), - ) - .route( - "/com.atproto.repo.listRecords", - get(api::repo::list_records), - ) - .route( - "/com.atproto.repo.describeRepo", - get(api::repo::describe_repo), - ) - .route("/com.atproto.repo.uploadBlob", post(api::repo::upload_blob)) - .route( - "/com.atproto.repo.applyWrites", - post(api::repo::apply_writes), - ) - .route( - "/com.atproto.sync.getLatestCommit", - get(sync::get_latest_commit), - ) - .route("/com.atproto.sync.listRepos", get(sync::list_repos)) - .route("/com.atproto.sync.getBlob", get(sync::get_blob)) - .route("/com.atproto.sync.listBlobs", get(sync::list_blobs)) - .route( - "/com.atproto.sync.getRepoStatus", - get(sync::get_repo_status), - ) - .route( - "/com.atproto.server.checkAccountStatus", - get(api::server::check_account_status), - ) - .route( - "/com.atproto.identity.getRecommendedDidCredentials", - get(api::identity::get_recommended_did_credentials), - ) - .route( - "/com.atproto.repo.listMissingBlobs", - get(api::repo::list_missing_blobs), - ) - .route( - "/com.atproto.sync.notifyOfUpdate", - post(sync::notify_of_update), - ) - .route("/com.atproto.sync.requestCrawl", post(sync::request_crawl)) - .route("/com.atproto.sync.getBlocks", get(sync::get_blocks)) - .route("/com.atproto.sync.getRepo", get(sync::get_repo)) - .route("/com.atproto.sync.getRecord", get(sync::get_record)) - .route( - "/com.atproto.sync.subscribeRepos", - get(sync::subscribe_repos), - ) - .route("/com.atproto.sync.getHead", get(sync::get_head)) - .route("/com.atproto.sync.getCheckout", get(sync::get_checkout)) - .route( - "/com.atproto.moderation.createReport", - post(api::moderation::create_report), - ) - .route( - "/com.atproto.admin.getAccountInfo", - get(api::admin::get_account_info), - ) - .route( - "/com.atproto.admin.getAccountInfos", - get(api::admin::get_account_infos), - ) - .route( - "/com.atproto.admin.searchAccounts", - get(api::admin::search_accounts), - ) - .route( - "/com.atproto.server.activateAccount", - post(api::server::activate_account), - ) - .route( - "/com.atproto.server.deactivateAccount", - post(api::server::deactivate_account), - ) - .route( - "/com.atproto.server.requestAccountDelete", - post(api::server::request_account_delete), - ) - .route( - "/com.atproto.server.deleteAccount", - post(api::server::delete_account), - ) - .route( - "/com.atproto.server.requestPasswordReset", - post(api::server::request_password_reset), - ) - .route( - "/com.atproto.server.resetPassword", - post(api::server::reset_password), - ) - .route( - "/_account.changePassword", - post(api::server::change_password), - ) - .route( - "/_account.removePassword", - post(api::server::remove_password), - ) - .route( - "/_account.setPassword", - post(api::server::set_password), - ) - .route( - "/_account.getPasswordStatus", - get(api::server::get_password_status), - ) - .route( - "/_account.getReauthStatus", - get(api::server::get_reauth_status), - ) - .route( - "/_account.reauthPassword", - post(api::server::reauth_password), - ) - .route("/_account.reauthTotp", post(api::server::reauth_totp)) - .route( - "/_account.reauthPasskeyStart", - post(api::server::reauth_passkey_start), - ) - .route( - "/_account.reauthPasskeyFinish", - post(api::server::reauth_passkey_finish), - ) - .route( - "/_account.getLegacyLoginPreference", - get(api::server::get_legacy_login_preference), - ) - .route( - "/_account.updateLegacyLoginPreference", - post(api::server::update_legacy_login_preference), - ) - .route("/_account.updateLocale", post(api::server::update_locale)) - .route( - "/_account.listTrustedDevices", - get(api::server::list_trusted_devices), - ) - .route( - "/_account.revokeTrustedDevice", - post(api::server::revoke_trusted_device), - ) - .route( - "/_account.updateTrustedDevice", - post(api::server::update_trusted_device), - ) - .route( - "/_account.createPasskeyAccount", - post(api::server::create_passkey_account), - ) - .route( - "/_account.startPasskeyRegistrationForSetup", - post(api::server::start_passkey_registration_for_setup), - ) - .route( - "/_account.completePasskeySetup", - post(api::server::complete_passkey_setup), - ) - .route( - "/_account.requestPasskeyRecovery", - post(api::server::request_passkey_recovery), - ) - .route( - "/_account.recoverPasskeyAccount", - post(api::server::recover_passkey_account), - ) - .route( - "/_account.updateDidDocument", - post(api::server::update_did_document), - ) - .route( - "/_account.getDidDocument", - get(api::server::get_did_document), - ) - .route( - "/com.atproto.server.requestEmailUpdate", - post(api::server::request_email_update), - ) - .route( - "/_checkEmailVerified", - post(api::server::check_email_verified), - ) - .route( - "/_checkChannelVerified", - post(api::server::check_channel_verified), - ) - .route( - "/com.atproto.server.confirmEmail", - post(api::server::confirm_email), - ) - .route( - "/com.atproto.server.updateEmail", - post(api::server::update_email), - ) - .route( - "/_account.authorizeEmailUpdate", - get(api::server::authorize_email_update), - ) - .route( - "/_account.checkEmailUpdateStatus", - get(api::server::check_email_update_status), - ) - .route( - "/_account.checkEmailInUse", - post(api::server::check_email_in_use), - ) - .route( - "/_account.checkCommsChannelInUse", - post(api::server::check_comms_channel_in_use), - ) - .route( - "/com.atproto.server.reserveSigningKey", - post(api::server::reserve_signing_key), - ) - .route( - "/com.atproto.server.verifyMigrationEmail", - post(api::server::verify_migration_email), - ) - .route( - "/com.atproto.server.resendMigrationVerification", - post(api::server::resend_migration_verification), - ) - .route( - "/com.atproto.identity.updateHandle", - post(api::identity::update_handle), - ) - .route( - "/com.atproto.identity.requestPlcOperationSignature", - post(api::identity::request_plc_operation_signature), - ) - .route( - "/com.atproto.identity.signPlcOperation", - post(api::identity::sign_plc_operation), - ) - .route( - "/com.atproto.identity.submitPlcOperation", - post(api::identity::submit_plc_operation), - ) - .route( - "/_identity.verifyHandleOwnership", - post(api::identity::verify_handle_ownership), - ) - .route("/com.atproto.repo.importRepo", post(api::repo::import_repo)) - .route( - "/com.atproto.admin.deleteAccount", - post(api::admin::delete_account), - ) - .route( - "/com.atproto.admin.updateAccountEmail", - post(api::admin::update_account_email), - ) - .route( - "/com.atproto.admin.updateAccountHandle", - post(api::admin::update_account_handle), - ) - .route( - "/com.atproto.admin.updateAccountPassword", - post(api::admin::update_account_password), - ) - .route( - "/com.atproto.server.listAppPasswords", - get(api::server::list_app_passwords), - ) - .route( - "/com.atproto.server.createAppPassword", - post(api::server::create_app_password), - ) - .route( - "/com.atproto.server.revokeAppPassword", - post(api::server::revoke_app_password), - ) - .route( - "/com.atproto.server.createInviteCode", - post(api::server::create_invite_code), - ) - .route( - "/com.atproto.server.createInviteCodes", - post(api::server::create_invite_codes), - ) - .route( - "/com.atproto.server.getAccountInviteCodes", - get(api::server::get_account_invite_codes), - ) - .route( - "/com.atproto.server.createTotpSecret", - post(api::server::create_totp_secret), - ) - .route( - "/com.atproto.server.enableTotp", - post(api::server::enable_totp), - ) - .route( - "/com.atproto.server.disableTotp", - post(api::server::disable_totp), - ) - .route( - "/com.atproto.server.getTotpStatus", - get(api::server::get_totp_status), - ) - .route( - "/com.atproto.server.regenerateBackupCodes", - post(api::server::regenerate_backup_codes), - ) - .route( - "/com.atproto.server.startPasskeyRegistration", - post(api::server::start_passkey_registration), - ) - .route( - "/com.atproto.server.finishPasskeyRegistration", - post(api::server::finish_passkey_registration), - ) - .route( - "/com.atproto.server.listPasskeys", - get(api::server::list_passkeys), - ) - .route( - "/com.atproto.server.deletePasskey", - post(api::server::delete_passkey), - ) - .route( - "/com.atproto.server.updatePasskey", - post(api::server::update_passkey), - ) - .route( - "/com.atproto.admin.getInviteCodes", - get(api::admin::get_invite_codes), - ) - .route("/_admin.getServerStats", get(api::admin::get_server_stats)) - .route("/_server.getConfig", get(api::admin::get_server_config)) - .route( - "/_admin.updateServerConfig", - post(api::admin::update_server_config), - ) - .route( - "/com.atproto.admin.disableAccountInvites", - post(api::admin::disable_account_invites), - ) - .route( - "/com.atproto.admin.enableAccountInvites", - post(api::admin::enable_account_invites), - ) - .route( - "/com.atproto.admin.disableInviteCodes", - post(api::admin::disable_invite_codes), - ) - .route( - "/com.atproto.admin.getSubjectStatus", - get(api::admin::get_subject_status), - ) - .route( - "/com.atproto.admin.updateSubjectStatus", - post(api::admin::update_subject_status), - ) - .route("/com.atproto.admin.sendEmail", post(api::admin::send_email)) - .route( - "/app.bsky.actor.getPreferences", - get(api::actor::get_preferences), - ) - .route( - "/app.bsky.actor.putPreferences", - post(api::actor::put_preferences), - ) - .route( - "/com.atproto.temp.checkSignupQueue", - get(api::temp::check_signup_queue), - ) - .route( - "/com.atproto.temp.dereferenceScope", - post(api::temp::dereference_scope), - ) - .route( - "/_account.getNotificationPrefs", - get(api::notification_prefs::get_notification_prefs), - ) - .route( - "/_account.updateNotificationPrefs", - post(api::notification_prefs::update_notification_prefs), - ) - .route( - "/_account.getNotificationHistory", - get(api::notification_prefs::get_notification_history), - ) - .route( - "/_account.confirmChannelVerification", - post(api::verification::confirm_channel_verification), - ) - .route("/_account.verifyToken", post(api::server::verify_token)) - .route( - "/_delegation.listControllers", - get(api::delegation::list_controllers), - ) - .route( - "/_delegation.addController", - post(api::delegation::add_controller), - ) - .route( - "/_delegation.removeController", - post(api::delegation::remove_controller), - ) - .route( - "/_delegation.updateControllerScopes", - post(api::delegation::update_controller_scopes), - ) - .route( - "/_delegation.listControlledAccounts", - get(api::delegation::list_controlled_accounts), - ) - .route( - "/_delegation.getAuditLog", - get(api::delegation::get_audit_log), - ) - .route( - "/_delegation.getScopePresets", - get(api::delegation::get_scope_presets), - ) - .route( - "/_delegation.createDelegatedAccount", - post(api::delegation::create_delegated_account), - ) - .route("/_backup.listBackups", get(api::backup::list_backups)) - .route("/_backup.getBackup", get(api::backup::get_backup)) - .route("/_backup.createBackup", post(api::backup::create_backup)) - .route("/_backup.deleteBackup", post(api::backup::delete_backup)) - .route("/_backup.setEnabled", post(api::backup::set_backup_enabled)) - .route("/_backup.exportBlobs", get(api::backup::export_blobs)) - .route( - "/app.bsky.ageassurance.getState", - get(api::age_assurance::get_state), - ) - .route( - "/app.bsky.unspecced.getAgeAssuranceState", - get(api::age_assurance::get_age_assurance_state), - ) + app_with_routes(state, ExternalRoutes::default()) +} + +pub fn app_with_routes(state: AppState, external: ExternalRoutes) -> Router { + let xrpc_router = external.xrpc .fallback(async || ( StatusCode::NOT_IMPLEMENTED, Json(json!({"error": "MethodNotImplemented", "message": "Method not implemented. For app.bsky.* methods, include an atproto-proxy header specifying your AppView."})), @@ -558,120 +93,16 @@ pub fn app(state: AppState) -> Router { .with_state(state.clone()), ); - let oauth_router = Router::new() - .route("/jwks", get(oauth::endpoints::oauth_jwks)) - .route("/par", post(oauth::endpoints::pushed_authorization_request)) - .route("/authorize", get(oauth::endpoints::authorize_get)) - .route("/authorize", post(oauth::endpoints::authorize_post)) - .route( - "/authorize/accounts", - get(oauth::endpoints::authorize_accounts), - ) - .route( - "/authorize/select", - post(oauth::endpoints::authorize_select), - ) - .route("/authorize/2fa", get(oauth::endpoints::authorize_2fa_get)) - .route("/authorize/2fa", post(oauth::endpoints::authorize_2fa_post)) - .route( - "/authorize/passkey", - get(oauth::endpoints::authorize_passkey_start), - ) - .route( - "/authorize/passkey", - post(oauth::endpoints::authorize_passkey_finish), - ) - .route( - "/passkey/check", - get(oauth::endpoints::check_user_has_passkeys), - ) - .route( - "/security-status", - get(oauth::endpoints::check_user_security_status), - ) - .route("/passkey/start", post(oauth::endpoints::passkey_start)) - .route("/passkey/finish", post(oauth::endpoints::passkey_finish)) - .route("/authorize/deny", post(oauth::endpoints::authorize_deny)) - .route( - "/register/complete", - post(oauth::endpoints::register_complete), - ) - .route( - "/establish-session", - post(oauth::endpoints::establish_session), - ) - .route("/authorize/consent", get(oauth::endpoints::consent_get)) - .route("/authorize/consent", post(oauth::endpoints::consent_post)) - .route("/authorize/renew", post(oauth::endpoints::authorize_renew)) - .route( - "/authorize/redirect", - get(oauth::endpoints::authorize_redirect), - ) - .route("/delegation/auth", post(oauth::endpoints::delegation_auth)) - .route( - "/delegation/auth-token", - post(oauth::endpoints::delegation_auth_token), - ) - .route( - "/delegation/totp", - post(oauth::endpoints::delegation_totp_verify), - ) - .route("/token", post(oauth::endpoints::token_endpoint)) - .route("/revoke", post(oauth::endpoints::revoke_token)) - .route("/introspect", post(oauth::endpoints::introspect_token)) - .route("/sso/providers", get(sso::endpoints::get_sso_providers)) - .route("/sso/initiate", post(sso::endpoints::sso_initiate)) - .route( - "/sso/callback", - get(sso::endpoints::sso_callback).post(sso::endpoints::sso_callback_post), - ) - .route("/sso/linked", get(sso::endpoints::get_linked_accounts)) - .route("/sso/unlink", post(sso::endpoints::unlink_account)) - .route( - "/sso/pending-registration", - get(sso::endpoints::get_pending_registration), - ) - .route( - "/sso/complete-registration", - post(sso::endpoints::complete_registration), - ) - .route( - "/sso/check-handle-available", - get(sso::endpoints::check_handle_available), - ) - .layer(middleware::from_fn(oauth::verify::dpop_nonce_middleware)); + let oauth_router = external.oauth; - let well_known_router = Router::new() - .route("/did.json", get(api::identity::well_known_did)) - .route("/atproto-did", get(api::identity::well_known_atproto_did)) - .route( - "/oauth-protected-resource", - get(oauth::endpoints::oauth_protected_resource), - ) - .route( - "/oauth-authorization-server", - get(oauth::endpoints::oauth_authorization_server), - ); + let well_known_router = external.well_known; let router = Router::new() .nest_service("/xrpc", xrpc_service) .nest("/oauth", oauth_router) .nest("/.well-known", well_known_router) .route("/metrics", get(metrics::metrics_handler)) - .route("/health", get(api::server::health)) - .route("/robots.txt", get(api::server::robots_txt)) - .route("/favicon.ico", get(api::server::get_logo)) - .route("/u/{handle}/did.json", get(api::identity::user_did_doc)) - .route( - "/webhook/telegram", - post(api::telegram_webhook::handle_telegram_webhook) - .layer(DefaultBodyLimit::max(64 * 1024)), - ) - .route( - "/webhook/discord", - post(api::discord_webhook::handle_discord_webhook) - .layer(DefaultBodyLimit::max(64 * 1024)), - ) + .merge(external.extra) .layer(DefaultBodyLimit::max( tranquil_config::get().server.max_blob_size as usize, )) @@ -717,10 +148,6 @@ pub fn app(state: AppState) -> Router { let serve_dir = ServeDir::new(frontend_dir).not_found_service(ServeFile::new(&index_path)); return router - .route( - "/oauth-client-metadata.json", - get(oauth::endpoints::frontend_client_metadata), - ) .route_service("/", ServeFile::new(&homepage_file)) .nest("/app", spa_router) .fallback_service(serve_dir); diff --git a/crates/tranquil-pds/src/oauth/mod.rs b/crates/tranquil-pds/src/oauth/mod.rs index f184714..faa620f 100644 --- a/crates/tranquil-pds/src/oauth/mod.rs +++ b/crates/tranquil-pds/src/oauth/mod.rs @@ -1,6 +1,4 @@ pub mod db; -pub mod endpoints; -pub mod jwks; pub mod scopes; pub mod verify; diff --git a/crates/tranquil-pds/src/api/repo/record/utils.rs b/crates/tranquil-pds/src/repo_ops.rs similarity index 100% rename from crates/tranquil-pds/src/api/repo/record/utils.rs rename to crates/tranquil-pds/src/repo_ops.rs diff --git a/crates/tranquil-pds/src/sso/mod.rs b/crates/tranquil-pds/src/sso/mod.rs index 56fd438..0c22c58 100644 --- a/crates/tranquil-pds/src/sso/mod.rs +++ b/crates/tranquil-pds/src/sso/mod.rs @@ -1,5 +1,4 @@ pub mod config; -pub mod endpoints; pub mod providers; pub use config::SsoConfig; diff --git a/crates/tranquil-pds/src/state.rs b/crates/tranquil-pds/src/state.rs index 0c3381d..da6a92f 100644 --- a/crates/tranquil-pds/src/state.rs +++ b/crates/tranquil-pds/src/state.rs @@ -8,7 +8,7 @@ use crate::repo::PostgresBlockStore; use crate::repo_write_lock::RepoWriteLocks; use crate::sso::{SsoConfig, SsoManager}; use crate::storage::{BackupStorage, BlobStorage, create_backup_storage, create_blob_storage}; -use crate::sync::firehose::SequencedEvent; +use tranquil_db_traits::SequencedEvent; use sqlx::PgPool; use std::error::Error; use std::sync::Arc; @@ -240,7 +240,7 @@ impl AppState { .await, ) { (true, Ok(Some(0))) => { - let code = crate::api::server::invite::gen_invite_code(); + let code = crate::util::gen_invite_code(); tracing::info!( "No users exist and invite codes are required. Bootstrap invite code: {}", code diff --git a/crates/tranquil-pds/src/sync/mod.rs b/crates/tranquil-pds/src/sync/mod.rs index f89e35e..54cbf99 100644 --- a/crates/tranquil-pds/src/sync/mod.rs +++ b/crates/tranquil-pds/src/sync/mod.rs @@ -1,24 +1,14 @@ -pub mod blob; pub mod car; -pub mod commit; -pub mod crawl; -pub mod deprecated; pub mod firehose; pub mod frame; pub mod import; -pub mod listener; -pub mod repo; -pub mod subscribe_repos; pub mod util; pub mod verify; -pub use blob::{get_blob, list_blobs}; -pub use commit::{get_latest_commit, get_repo_status, list_repos}; -pub use crawl::{notify_of_update, request_crawl}; -pub use deprecated::{get_checkout, get_head}; -pub use repo::{get_blocks, get_record, get_repo}; -pub use subscribe_repos::subscribe_repos; -pub use tranquil_db_traits::AccountStatus; +#[cfg(test)] +mod verify_tests; + +pub use firehose::SequencedEvent; pub use util::{ RepoAccessLevel, RepoAccount, RepoAvailabilityError, assert_repo_availability, get_account_with_status, diff --git a/crates/tranquil-pds/src/sync/verify.rs b/crates/tranquil-pds/src/sync/verify.rs index 4aeeefd..923ddaa 100644 --- a/crates/tranquil-pds/src/sync/verify.rs +++ b/crates/tranquil-pds/src/sync/verify.rs @@ -130,7 +130,7 @@ impl CarVerifier { .ok_or(VerifyError::NoSigningKey) } - async fn resolve_did_document(&self, did: &Did) -> Result, VerifyError> { + pub(crate) async fn resolve_did_document(&self, did: &Did) -> Result, VerifyError> { let did_str = did.as_str(); if did_str.starts_with("did:plc:") { self.resolve_plc_did(did_str).await @@ -197,7 +197,7 @@ impl CarVerifier { Ok(doc.into_static()) } - fn verify_mst_structure( + pub(crate) fn verify_mst_structure( &self, data_cid: &Cid, blocks: &HashMap, diff --git a/crates/tranquil-pds/src/sync/verify_tests.rs b/crates/tranquil-pds/src/sync/verify_tests.rs index 80732a6..80aef48 100644 --- a/crates/tranquil-pds/src/sync/verify_tests.rs +++ b/crates/tranquil-pds/src/sync/verify_tests.rs @@ -1,4 +1,4 @@ -use crate::sync::verify::{CarVerifier, VerifyError}; +use super::{CarVerifier, VerifyError}; use bytes::Bytes; use cid::Cid; use sha2::{Digest, Sha256}; diff --git a/crates/tranquil-pds/src/util.rs b/crates/tranquil-pds/src/util.rs index 5e42f2c..c2cb32e 100644 --- a/crates/tranquil-pds/src/util.rs +++ b/crates/tranquil-pds/src/util.rs @@ -535,3 +535,24 @@ mod tests { ); } } + +pub(crate) fn gen_invite_random_token() -> String { + let mut rng = rand::thread_rng(); + let chars: Vec = BASE32_ALPHABET.chars().collect(); + let gen_segment = |rng: &mut rand::rngs::ThreadRng, len: usize| -> String { + (0..len) + .map(|_| chars[rng.gen_range(0..chars.len())]) + .collect() + }; + format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5)) +} + +pub fn gen_invite_code() -> String { + let hostname = &tranquil_config::get().server.hostname; + let hostname_prefix = hostname.replace('.', "-"); + format!("{}-{}", hostname_prefix, gen_invite_random_token()) +} + +pub fn is_self_hosted_did_web_enabled() -> bool { + tranquil_config::get().server.enable_pds_hosted_did_web +} diff --git a/crates/tranquil-pds/tests/commit_signing.rs b/crates/tranquil-pds/tests/commit_signing.rs index df8d237..3c3554e 100644 --- a/crates/tranquil-pds/tests/commit_signing.rs +++ b/crates/tranquil-pds/tests/commit_signing.rs @@ -96,7 +96,7 @@ fn test_unsigned_commit_has_5_fields() { #[test] fn test_create_signed_commit_helper() { - use tranquil_pds::api::repo::record::utils::create_signed_commit; + use tranquil_pds::repo_ops::create_signed_commit; let signing_key = SigningKey::random(&mut rand::thread_rng()); let did: Did = "did:plc:testuser123456789abcdef" diff --git a/crates/tranquil-pds/tests/common/mod.rs b/crates/tranquil-pds/tests/common/mod.rs index cd4d2de..ddea18b 100644 --- a/crates/tranquil-pds/tests/common/mod.rs +++ b/crates/tranquil-pds/tests/common/mod.rs @@ -563,8 +563,19 @@ async fn spawn_server(config: ServerConfig) -> ServerInstance { if let Some((cache, distributed_rate_limiter)) = config.cache { state = state.with_cache(cache, distributed_rate_limiter); } - tranquil_pds::sync::listener::start_sequencer_listener(state.clone()).await; - let app = tranquil_pds::app(state); + tranquil_sync::listener::start_sequencer_listener(state.clone()).await; + let app = tranquil_pds::app_with_routes( + state, + tranquil_pds::ExternalRoutes { + xrpc: tranquil_api::api_routes().merge(tranquil_sync::sync_routes()), + oauth: tranquil_oauth_server::oauth_routes(), + well_known: tranquil_oauth_server::well_known_oauth_routes() + .merge(tranquil_api::well_known_api_routes()), + extra: tranquil_api::misc_routes() + .merge(tranquil_api::webhook_routes()) + .merge(tranquil_oauth_server::frontend_client_metadata_route()), + }, + ); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); diff --git a/crates/tranquil-server/Cargo.toml b/crates/tranquil-server/Cargo.toml new file mode 100644 index 0000000..fab4f79 --- /dev/null +++ b/crates/tranquil-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tranquil-server" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-pds = { workspace = true } +tranquil-sync = { workspace = true } +tranquil-api = { workspace = true } +tranquil-oauth-server = { workspace = true } +tranquil-config = { workspace = true } + +axum = { workspace = true } +clap = { workspace = true } +dotenvy = { workspace = true } +ed25519-dalek = { workspace = true } +hex = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[features] +default = ["frontend", "s3", "valkey"] +frontend = ["tranquil-pds/frontend"] +s3 = ["tranquil-pds/s3"] +valkey = ["tranquil-pds/valkey"] diff --git a/crates/tranquil-server/build.rs b/crates/tranquil-server/build.rs new file mode 100644 index 0000000..09dec18 --- /dev/null +++ b/crates/tranquil-server/build.rs @@ -0,0 +1,12 @@ +use std::process::Command; + +fn main() { + let timestamp = Command::new("date") + .arg("+%Y-%m-%d %H:%M:%S UTC") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + + println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/crates/tranquil-pds/src/main.rs b/crates/tranquil-server/src/main.rs similarity index 94% rename from crates/tranquil-pds/src/main.rs rename to crates/tranquil-server/src/main.rs index ce84f48..12a57da 100644 --- a/crates/tranquil-pds/src/main.rs +++ b/crates/tranquil-server/src/main.rs @@ -18,7 +18,6 @@ use tranquil_pds::state::AppState; #[derive(Parser)] #[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")] struct Cli { - /// Path to a TOML configuration file (also settable via TRANQUIL_PDS_CONFIG env var) #[arg(short, long, value_name = "FILE", env = "TRANQUIL_PDS_CONFIG")] config: Option, @@ -28,14 +27,10 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Validate the configuration and exit Validate { - /// Skip validation of secrets and database URL (useful when secrets - /// are provided at runtime via environment variables / secret files) #[arg(long)] ignore_secrets: bool, }, - /// Print a TOML configuration template to stdout ConfigTemplate, } @@ -45,7 +40,6 @@ async fn main() -> ExitCode { let cli = Cli::parse(); - // Handle subcommands that don't need full startup if let Some(command) = &cli.command { return match command { Command::ConfigTemplate => { @@ -116,7 +110,7 @@ async fn run() -> Result<(), Box> { spawn_signal_handler(shutdown.clone()); let state = AppState::new(shutdown.clone()).await?; - tranquil_pds::sync::listener::start_sequencer_listener(state.clone()).await; + tranquil_sync::listener::start_sequencer_listener(state.clone()).await; let backfill_repo_repo = state.repo_repo.clone(); let backfill_block_store = state.block_store.clone(); @@ -189,7 +183,6 @@ async fn run() -> Result<(), Box> { } if let Some(telegram_sender) = TelegramSender::from_config(cfg) { - // Safe to unwrap: validated in TranquilConfig::validate() let secret_token = tranquil_config::get() .telegram .webhook_secret @@ -262,7 +255,18 @@ async fn run() -> Result<(), Box> { shutdown.clone(), )); - let app = tranquil_pds::app(state); + let app = tranquil_pds::app_with_routes( + state, + tranquil_pds::ExternalRoutes { + xrpc: tranquil_api::api_routes().merge(tranquil_sync::sync_routes()), + oauth: tranquil_oauth_server::oauth_routes(), + well_known: tranquil_oauth_server::well_known_oauth_routes() + .merge(tranquil_api::well_known_api_routes()), + extra: tranquil_api::misc_routes() + .merge(tranquil_api::webhook_routes()) + .merge(tranquil_oauth_server::frontend_client_metadata_route()), + }, + ); let cfg = tranquil_config::get(); let host = &cfg.server.host; diff --git a/crates/tranquil-sync/Cargo.toml b/crates/tranquil-sync/Cargo.toml new file mode 100644 index 0000000..c9382f7 --- /dev/null +++ b/crates/tranquil-sync/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tranquil-sync" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tranquil-pds = { workspace = true } +tranquil-types = { workspace = true } +tranquil-config = { workspace = true } +tranquil-db-traits = { workspace = true } + +anyhow = { workspace = true } +axum = { workspace = true } +bytes = { workspace = true } +chrono = { workspace = true } +cid = { workspace = true } +futures = { workspace = true } +ipld-core = { workspace = true } +jacquard-repo = { workspace = true } +serde = { workspace = true } +serde_ipld_dagcbor = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/tranquil-pds/src/sync/blob.rs b/crates/tranquil-sync/src/blob.rs similarity index 96% rename from crates/tranquil-pds/src/sync/blob.rs rename to crates/tranquil-sync/src/blob.rs index 47e5aab..d77460f 100644 --- a/crates/tranquil-pds/src/sync/blob.rs +++ b/crates/tranquil-sync/src/blob.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::state::AppState; -use crate::sync::util::{RepoAccessLevel, assert_repo_availability}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::util::{RepoAccessLevel, assert_repo_availability}; use axum::{ Json, body::Body, diff --git a/crates/tranquil-pds/src/sync/commit.rs b/crates/tranquil-sync/src/commit.rs similarity index 97% rename from crates/tranquil-pds/src/sync/commit.rs rename to crates/tranquil-sync/src/commit.rs index d206763..994afce 100644 --- a/crates/tranquil-pds/src/sync/commit.rs +++ b/crates/tranquil-sync/src/commit.rs @@ -1,6 +1,6 @@ -use crate::api::error::ApiError; -use crate::state::AppState; -use crate::sync::util::{RepoAccessLevel, assert_repo_availability, get_account_with_status}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::util::{RepoAccessLevel, assert_repo_availability, get_account_with_status}; use axum::{ Json, extract::{Query, State}, diff --git a/crates/tranquil-pds/src/sync/crawl.rs b/crates/tranquil-sync/src/crawl.rs similarity index 91% rename from crates/tranquil-pds/src/sync/crawl.rs rename to crates/tranquil-sync/src/crawl.rs index 3bc239d..801f9c4 100644 --- a/crates/tranquil-pds/src/sync/crawl.rs +++ b/crates/tranquil-sync/src/crawl.rs @@ -1,5 +1,5 @@ -use crate::api::EmptyResponse; -use crate::state::AppState; +use tranquil_pds::api::EmptyResponse; +use tranquil_pds::state::AppState; use axum::{ Json, extract::{Query, State}, diff --git a/crates/tranquil-pds/src/sync/deprecated.rs b/crates/tranquil-sync/src/deprecated.rs similarity index 89% rename from crates/tranquil-pds/src/sync/deprecated.rs rename to crates/tranquil-sync/src/deprecated.rs index 6164075..953164d 100644 --- a/crates/tranquil-pds/src/sync/deprecated.rs +++ b/crates/tranquil-sync/src/deprecated.rs @@ -1,7 +1,7 @@ -use crate::api::error::ApiError; -use crate::state::AppState; -use crate::sync::car::{encode_car_block, encode_car_header}; -use crate::sync::util::{RepoAccessLevel, assert_repo_availability}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::car::{encode_car_block, encode_car_header}; +use tranquil_pds::sync::util::{RepoAccessLevel, assert_repo_availability}; use axum::{ Json, extract::{Query, State}, @@ -18,16 +18,16 @@ use tranquil_types::Did; const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &Did) -> bool { - let extracted = match crate::auth::extract_auth_token_from_header(crate::util::get_header_str( + let extracted = match tranquil_pds::auth::extract_auth_token_from_header(tranquil_pds::util::get_header_str( headers, axum::http::header::AUTHORIZATION, )) { Some(t) => t, None => return false, }; - let dpop_proof = crate::util::get_header_str(headers, crate::util::HEADER_DPOP); + let dpop_proof = tranquil_pds::util::get_header_str(headers, tranquil_pds::util::HEADER_DPOP); let http_uri = "/"; - match crate::auth::validate_token_with_dpop( + match tranquil_pds::auth::validate_token_with_dpop( state.user_repo.as_ref(), state.oauth_repo.as_ref(), &extracted.token, @@ -35,7 +35,7 @@ async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &Did) - dpop_proof, Method::GET.as_str(), http_uri, - crate::auth::AccountRequirement::AnyStatus, + tranquil_pds::auth::AccountRequirement::AnyStatus, ) .await { diff --git a/crates/tranquil-sync/src/lib.rs b/crates/tranquil-sync/src/lib.rs new file mode 100644 index 0000000..126eecf --- /dev/null +++ b/crates/tranquil-sync/src/lib.rs @@ -0,0 +1,38 @@ +pub mod blob; +pub mod commit; +pub mod crawl; +pub mod deprecated; +pub mod listener; +pub mod repo; +pub mod subscribe_repos; + +pub use blob::{get_blob, list_blobs}; +pub use commit::{get_latest_commit, get_repo_status, list_repos}; +pub use crawl::{notify_of_update, request_crawl}; +pub use deprecated::{get_checkout, get_head}; +pub use repo::{get_blocks, get_record, get_repo}; +pub use subscribe_repos::subscribe_repos; + +use tranquil_pds::state::AppState; + +pub fn sync_routes() -> axum::Router { + use axum::routing::{get, post}; + + axum::Router::new() + .route("/com.atproto.sync.getLatestCommit", get(get_latest_commit)) + .route("/com.atproto.sync.listRepos", get(list_repos)) + .route("/com.atproto.sync.getBlob", get(get_blob)) + .route("/com.atproto.sync.listBlobs", get(list_blobs)) + .route("/com.atproto.sync.getRepoStatus", get(get_repo_status)) + .route("/com.atproto.sync.notifyOfUpdate", post(notify_of_update)) + .route("/com.atproto.sync.requestCrawl", post(request_crawl)) + .route("/com.atproto.sync.getBlocks", get(get_blocks)) + .route("/com.atproto.sync.getRepo", get(get_repo)) + .route("/com.atproto.sync.getRecord", get(get_record)) + .route( + "/com.atproto.sync.subscribeRepos", + get(subscribe_repos), + ) + .route("/com.atproto.sync.getHead", get(get_head)) + .route("/com.atproto.sync.getCheckout", get(get_checkout)) +} diff --git a/crates/tranquil-pds/src/sync/listener.rs b/crates/tranquil-sync/src/listener.rs similarity index 98% rename from crates/tranquil-pds/src/sync/listener.rs rename to crates/tranquil-sync/src/listener.rs index bba0bea..4cd7a55 100644 --- a/crates/tranquil-pds/src/sync/listener.rs +++ b/crates/tranquil-sync/src/listener.rs @@ -1,5 +1,5 @@ -use crate::state::AppState; -use crate::sync::firehose::SequencedEvent; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::firehose::SequencedEvent; use std::sync::atomic::{AtomicI64, Ordering}; use tracing::{debug, error, info, warn}; use tranquil_db_traits::SequenceNumber; diff --git a/crates/tranquil-pds/src/sync/repo.rs b/crates/tranquil-sync/src/repo.rs similarity index 94% rename from crates/tranquil-pds/src/sync/repo.rs rename to crates/tranquil-sync/src/repo.rs index 40afaa4..ef710d6 100644 --- a/crates/tranquil-pds/src/sync/repo.rs +++ b/crates/tranquil-sync/src/repo.rs @@ -1,8 +1,8 @@ -use crate::api::error::ApiError; -use crate::scheduled::generate_repo_car_from_user_blocks; -use crate::state::AppState; -use crate::sync::car::{encode_car_block, encode_car_header}; -use crate::sync::util::{RepoAccessLevel, assert_repo_availability}; +use tranquil_pds::api::error::ApiError; +use tranquil_pds::scheduled::generate_repo_car_from_user_blocks; +use tranquil_pds::state::AppState; +use tranquil_pds::sync::car::{encode_car_block, encode_car_header}; +use tranquil_pds::sync::util::{RepoAccessLevel, assert_repo_availability}; use axum::{ extract::{Query, RawQuery, State}, http::StatusCode, @@ -21,14 +21,14 @@ struct GetBlocksParams { } fn parse_get_blocks_query(query_string: &str) -> Result { - let did_str = crate::util::parse_repeated_query_param(Some(query_string), "did") + let did_str = tranquil_pds::util::parse_repeated_query_param(Some(query_string), "did") .into_iter() .next() .ok_or_else(|| ApiError::InvalidRequest("Missing required parameter: did".into()))?; let did: Did = did_str .parse() .map_err(|_| ApiError::InvalidRequest("invalid did".into()))?; - let cids = crate::util::parse_repeated_query_param(Some(query_string), "cids"); + let cids = tranquil_pds::util::parse_repeated_query_param(Some(query_string), "cids"); Ok(GetBlocksParams { did, cids }) } @@ -90,7 +90,7 @@ pub async fn get_blocks(State(state): State, RawQuery(query): RawQuery .into_response(); } - let header = match crate::sync::car::encode_car_header_null_root() { + let header = match tranquil_pds::sync::car::encode_car_header_null_root() { Ok(h) => h, Err(e) => { error!("Failed to encode CAR header: {}", e); diff --git a/crates/tranquil-pds/src/sync/subscribe_repos.rs b/crates/tranquil-sync/src/subscribe_repos.rs similarity index 94% rename from crates/tranquil-pds/src/sync/subscribe_repos.rs rename to crates/tranquil-sync/src/subscribe_repos.rs index 578aa61..93a66aa 100644 --- a/crates/tranquil-pds/src/sync/subscribe_repos.rs +++ b/crates/tranquil-sync/src/subscribe_repos.rs @@ -1,7 +1,7 @@ -use crate::state::AppState; -use crate::sync::firehose::SequencedEvent; -use crate::sync::frame::{ErrorFrameName, InfoFrameName}; -use crate::sync::util::{ +use tranquil_pds::state::AppState; +use tranquil_pds::sync::firehose::SequencedEvent; +use tranquil_pds::sync::frame::{ErrorFrameName, InfoFrameName}; +use tranquil_pds::sync::util::{ format_error_frame, format_event_for_sending, format_event_with_prefetched_blocks, format_info_frame, prefetch_blocks_for_events, }; @@ -50,11 +50,11 @@ pub fn get_subscriber_count() -> usize { async fn handle_socket(mut socket: WebSocket, state: AppState, params: SubscribeReposParams) { let count = SUBSCRIBER_COUNT.fetch_add(1, Ordering::SeqCst) + 1; - crate::metrics::set_firehose_subscribers(count); + tranquil_pds::metrics::set_firehose_subscribers(count); info!(cursor = ?params.cursor, subscribers = count, "New firehose subscriber"); let _ = handle_socket_inner(&mut socket, &state, params).await; let count = SUBSCRIBER_COUNT.fetch_sub(1, Ordering::SeqCst) - 1; - crate::metrics::set_firehose_subscribers(count); + tranquil_pds::metrics::set_firehose_subscribers(count); info!(subscribers = count, "Firehose subscriber disconnected"); } @@ -157,7 +157,7 @@ async fn handle_socket_inner( warn!("Failed to send backfill event: {}", e); return Err(()); } - crate::metrics::record_firehose_event(); + tranquil_pds::metrics::record_firehose_event(); } if i64::try_from(events_count).unwrap_or(i64::MAX) < BACKFILL_BATCH_SIZE { break; @@ -197,7 +197,7 @@ async fn handle_socket_inner( warn!("Failed to send cutover event: {}", e); return Err(()); } - crate::metrics::record_firehose_event(); + tranquil_pds::metrics::record_firehose_event(); } } } @@ -215,7 +215,7 @@ async fn handle_socket_inner( warn!("Failed to send event: {}", e); break; } - crate::metrics::record_firehose_event(); + tranquil_pds::metrics::record_firehose_event(); } Err(RecvError::Lagged(skipped)) => { warn!(skipped = skipped, "Firehose subscriber lagged behind"); diff --git a/default.nix b/default.nix index 11905f8..140b229 100644 --- a/default.nix +++ b/default.nix @@ -36,6 +36,6 @@ in rustPlatform.buildRustPackage { meta = { license = lib.licenses.agpl3Plus; - mainProgram = "tranquil-pds"; + mainProgram = "tranquil-server"; }; } diff --git a/justfile b/justfile index 848622a..c5fd30d 100644 --- a/justfile +++ b/justfile @@ -2,9 +2,9 @@ default: @just --list run: - cargo run + cargo run -p tranquil-server run-release: - cargo run --release + cargo run -p tranquil-server --release build: cargo build build-release: diff --git a/scripts/install-debian.sh b/scripts/install-debian.sh index 23bc194..c67bd46 100755 --- a/scripts/install-debian.sh +++ b/scripts/install-debian.sh @@ -317,7 +317,7 @@ chmod 600 /etc/tranquil-pds/tranquil-pds.env log_info "Installing Tranquil PDS..." id -u tranquil-pds &>/dev/null || useradd -r -s /sbin/nologin tranquil-pds -cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ +cp /opt/tranquil-pds/target/release/tranquil-server /usr/local/bin/tranquil-pds mkdir -p /var/lib/tranquil-pds cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds diff --git a/test.nix b/test.nix index 4ccaa0b..b680f0b 100644 --- a/test.nix +++ b/test.nix @@ -17,23 +17,38 @@ pkgs.testers.nixosTest { enable = true; database.createLocally = true; - nginx = { - enable = true; - enableACME = false; - }; - settings = { - PDS_HOSTNAME = "pds.test"; - SERVER_HOST = "0.0.0.0"; + server.hostname = "pds.test"; + server.host = "0.0.0.0"; + server.disable_rate_limiting = true; + server.invite_code_required = false; + server.enable_pds_hosted_did_web = true; - DISABLE_RATE_LIMITING = 1; - TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = 1; - - JWT_SECRET="test-jwt-secret-must-be-32-chars-long"; - DPOP_SECRET="test-dpop-secret-must-be-32-chars-long"; - MASTER_KEY="test-master-key-must-be-32-chars-long"; + secrets.jwt_secret = "test-jwt-secret-must-be-32-chars-long"; + secrets.dpop_secret = "test-dpop-secret-must-be-32-chars-long"; + secrets.master_key = "test-master-key-must-be-32-chars-long"; + secrets.allow_insecure = true; }; }; + + services.nginx = let + vhost = { + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + ''; + }; + }; + in { + enable = true; + recommendedProxySettings = true; + virtualHosts."pds.test" = vhost; + virtualHosts."*.pds.test" = vhost; + }; }; testScript = '' @@ -50,7 +65,7 @@ pkgs.testers.nixosTest { base = "http://localhost" if via == "nginx" else "http://localhost:3000" url = f"{base}/xrpc/{endpoint}" - parts = ["curl", "-sf", "-X", method, host_header] + parts = ["curl", "-s", "-w", r"'\n%{http_code}'", "-X", method, host_header] if headers: parts.extend(f"-H '{k}: {v}'" for k, v in headers.items()) if data is not None: @@ -60,7 +75,12 @@ pkgs.testers.nixosTest { parts.append(f"--data-binary @{raw_body}") parts.append(f"'{url}'") - return server.succeed(" ".join(parts)) + result = server.succeed(" ".join(parts)) + lines = result.rsplit("\n", 1) + body = lines[0] if len(lines) > 1 else result + status = lines[1].strip() if len(lines) > 1 else "000" + assert status.startswith("2"), f"xrpc {endpoint} returned HTTP {status}: {body}" + return body def xrpc_json(method, endpoint, **kwargs): return json.loads(xrpc(method, endpoint, **kwargs))