Compare commits

..
6 Commits
Author SHA1 Message Date
LewisandTangled 7926c798c6 feat: cross-pds delegation 2026-03-17 19:22:34 +00:00
LewisandTangled d5ed420dd7 fix: move code to more correct crates 2026-03-17 10:16:36 +00:00
Not HerselfandTangled 92e609d367 fix: handle AT Protocol $bytes type in json_to_ipld
json_to_ipld handled $link (→ Ipld::Link) but treated $bytes objects
as regular maps, producing a CBOR map (major type 5) instead of a byte
string (major type 2). This caused downstream consumers expecting
spec-compliant CBOR — notably Jetstream's atdata.UnmarshalCBOR — to
fail with "decoding $byte value: illegal base64 data".

Decode $bytes from standard base64 (RFC 4648 §4, padding optional)
into Ipld::Bytes, matching the existing $link handling pattern.
2026-03-15 07:15:07 +00:00
Lewis 546d342136 fix(auth): use authextractor for serviceauth too now 2026-03-14 13:04:14 +02:00
Lewis c680f3c419 fix(homepage): favicon should render in title
# I am sorry I forgot this.

Now pds.ls will show beautiful icons when showing Tranquil PDSes.
2026-03-14 11:57:01 +02:00
Lewis 806cb4b8c5 chore(build): optimize container cache layers 2026-03-14 11:53:25 +02:00
152 changed files with 4120 additions and 3368 deletions
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n d.controller_did,\n u.handle as \"handle?\",\n d.granted_scopes,\n d.granted_at,\n true as \"is_active!\",\n u.did IS NOT NULL as \"is_local!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))\n ORDER BY d.granted_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "controller_did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "handle?",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "granted_scopes",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "granted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "is_active!",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "is_local!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
null,
null
]
},
"hash": "d8e646324c93b375cceccea533ddd880225931f29ce4d8c5184197fecce25fa7"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n d.controller_did,\n u.handle as \"handle?\",\n d.granted_scopes,\n d.granted_at,\n CASE WHEN u.did IS NOT NULL\n THEN u.deactivated_at IS NULL AND u.takedown_ref IS NULL\n ELSE true\n END as \"is_active!\",\n u.did IS NOT NULL as \"is_local!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1 AND d.revoked_at IS NULL\n ORDER BY d.granted_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "controller_did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "handle?",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "granted_scopes",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "granted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "is_active!",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "is_local!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
null,
null
]
},
"hash": "dd6021dd12823e042b011b2c1507736a46c0dcf0eb94cb41de58f8ca0b3a2f08"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "ff2ffeb1ea1c1375ff0edc4c9ce2f3cdeb92e2b0a72405ea0441b0340b177b96"
}
Generated
+142 -19
View File
@@ -6092,9 +6092,57 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "tranquil-api"
version = "0.4.3"
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.3.1"
version = "0.4.3"
dependencies = [
"anyhow",
"base32",
@@ -6117,7 +6165,7 @@ dependencies = [
[[package]]
name = "tranquil-cache"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6131,7 +6179,7 @@ dependencies = [
[[package]]
name = "tranquil-comms"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6146,7 +6194,7 @@ dependencies = [
[[package]]
name = "tranquil-config"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"confique",
"serde",
@@ -6154,7 +6202,7 @@ dependencies = [
[[package]]
name = "tranquil-crypto"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"aes-gcm",
"base64 0.22.1",
@@ -6170,7 +6218,7 @@ dependencies = [
[[package]]
name = "tranquil-db"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"chrono",
@@ -6187,7 +6235,7 @@ dependencies = [
[[package]]
name = "tranquil-db-traits"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6203,7 +6251,7 @@ dependencies = [
[[package]]
name = "tranquil-infra"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"bytes",
@@ -6214,7 +6262,7 @@ dependencies = [
[[package]]
name = "tranquil-lexicon"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"chrono",
"hickory-resolver",
@@ -6232,7 +6280,7 @@ dependencies = [
[[package]]
name = "tranquil-oauth"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"anyhow",
"axum",
@@ -6253,9 +6301,42 @@ dependencies = [
"uuid",
]
[[package]]
name = "tranquil-oauth-server"
version = "0.4.3"
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.3.1"
version = "0.4.3"
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.3.1"
version = "0.4.3"
dependencies = [
"bytes",
"cid",
@@ -6355,7 +6436,7 @@ dependencies = [
[[package]]
name = "tranquil-ripple"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"backon",
@@ -6380,7 +6461,7 @@ dependencies = [
[[package]]
name = "tranquil-scopes"
version = "0.3.1"
version = "0.4.3"
dependencies = [
"axum",
"futures",
@@ -6394,9 +6475,29 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "tranquil-server"
version = "0.4.3"
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.3.1"
version = "0.4.3"
dependencies = [
"async-trait",
"aws-config",
@@ -6411,9 +6512,31 @@ dependencies = [
"uuid",
]
[[package]]
name = "tranquil-sync"
version = "0.4.3"
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.3.1"
version = "0.4.3"
dependencies = [
"chrono",
"cid",
+10 -1
View File
@@ -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.0"
version = "0.4.3"
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"
+25 -5
View File
@@ -4,21 +4,41 @@ COPY frontend/ ./
RUN deno task build
FROM rust:1.92-alpine AS builder
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
WORKDIR /app
ARG SLIM="false"
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
COPY .sqlx ./.sqlx
COPY crates/tranquil-types ./crates/tranquil-types
COPY crates/tranquil-crypto ./crates/tranquil-crypto
COPY crates/tranquil-scopes ./crates/tranquil-scopes
COPY crates/tranquil-config ./crates/tranquil-config
COPY crates/tranquil-repo ./crates/tranquil-repo
COPY crates/tranquil-lexicon ./crates/tranquil-lexicon
COPY crates/tranquil-oauth ./crates/tranquil-oauth
COPY crates/tranquil-db-traits ./crates/tranquil-db-traits
COPY crates/tranquil-infra ./crates/tranquil-infra
COPY crates/tranquil-auth ./crates/tranquil-auth
COPY crates/tranquil-comms ./crates/tranquil-comms
COPY crates/tranquil-db ./crates/tranquil-db
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
+50
View File
@@ -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 }
@@ -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,
@@ -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())
}
@@ -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,
@@ -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<Admin>,
RawQuery(raw_query): RawQuery,
) -> Result<Response, ApiError> {
let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
let dids: Vec<String> = tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
.into_iter()
.filter(|d| !d.is_empty())
.collect();
@@ -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},
@@ -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);
}
@@ -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};
@@ -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},
@@ -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,
@@ -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((
@@ -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<String> {
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(
@@ -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<AppState>,
auth: Auth<Active>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
Query(query): Query<GetBackupQuery>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
Query(query): Query<DeleteBackupQuery>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
Json(input): Json<SetBackupEnabledInput>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await {
Ok(Some(id)) => id,
Ok(None) => {
@@ -1,41 +1,23 @@
use crate::api::error::ApiError;
use crate::api::repo::record::utils::create_signed_commit;
use crate::auth::{Active, Auth};
use crate::delegation::{
use crate::identity::provision::{create_plc_did, init_genesis_repo};
use tranquil_pds::api::error::ApiError;
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,
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},
http::StatusCode,
response::{IntoResponse, Response},
};
use jacquard_common::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{error, info, warn};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
pub is_active: bool,
}
#[derive(Debug, Serialize)]
pub struct ListControllersResponse {
pub controllers: Vec<ControllerInfo>,
}
pub async fn list_controllers(
State(state): State<AppState>,
auth: Auth<Active>,
@@ -54,19 +36,23 @@ pub async fn list_controllers(
}
};
Ok(Json(ListControllersResponse {
controllers: controllers
.into_iter()
.map(|c| ControllerInfo {
did: c.did,
handle: c.handle,
granted_scopes: c.granted_scopes.into_string(),
granted_at: c.granted_at,
is_active: c.is_active,
})
.collect(),
})
.into_response())
let resolve_futures = controllers.into_iter().map(|mut c| {
let did_resolver = state.did_resolver.clone();
async move {
if c.handle.is_none() {
c.handle = did_resolver
.resolve_did_document(c.did.as_str())
.await
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
.map(|h| h.into());
}
c
}
});
let controllers = futures::future::join_all(resolve_futures).await;
Ok(Json(serde_json::json!({ "controllers": controllers })).into_response())
}
#[derive(Debug, Deserialize)]
@@ -80,16 +66,39 @@ pub async fn add_controller(
auth: Auth<Active>,
Json(input): Json<AddControllerInput>,
) -> Result<Response, ApiError> {
let controller_exists = state
.user_repo
.get_by_did(&input.controller_did)
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
.await
.ok()
.flatten()
.is_some();
.ok_or(ApiError::ControllerNotFound)?;
if !controller_exists {
return Ok(ApiError::ControllerNotFound.into_response());
if !resolved.is_local {
if let Some(ref pds_url) = resolved.pds_url {
if !pds_url.starts_with("https://") {
return Ok(
ApiError::InvalidDelegation("Controller PDS must use HTTPS".into())
.into_response(),
);
}
match state
.cross_pds_oauth
.check_remote_is_delegated(pds_url, input.controller_did.as_str())
.await
{
Some(true) => {
return Ok(ApiError::InvalidDelegation(
"Cannot add a delegated account from another PDS as a controller".into(),
)
.into_response());
}
Some(false) => {}
None => {
warn!(
controller = %input.controller_did,
pds = %pds_url,
"Could not verify remote controller delegation status"
);
}
}
}
}
let can_add = match verify_can_add_controllers(&state, &auth).await {
@@ -97,16 +106,19 @@ pub async fn add_controller(
Err(response) => return Ok(response),
};
let can_be_controller = match verify_can_be_controller(&state, &input.controller_did).await {
Ok(proof) => proof,
Err(response) => return Ok(response),
};
if resolved.is_local {
if state.delegation_repo.is_delegated_account(&input.controller_did).await.unwrap_or(false) {
return Ok(ApiError::InvalidDelegation(
"Cannot add a controlled account as a controller".into(),
).into_response());
}
}
match state
.delegation_repo
.create_delegation(
can_add.did(),
can_be_controller.did(),
&input.controller_did,
&input.granted_scopes,
can_add.did(),
)
@@ -118,10 +130,11 @@ pub async fn add_controller(
.log_delegation_action(
can_add.did(),
can_add.did(),
Some(can_be_controller.did()),
Some(&input.controller_did),
DelegationActionType::GrantCreated,
Some(serde_json::json!({
"granted_scopes": input.granted_scopes.as_str()
"granted_scopes": input.granted_scopes.as_str(),
"is_local": resolved.is_local
})),
None,
None,
@@ -256,20 +269,6 @@ pub async fn update_controller_scopes(
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegatedAccountInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct ListControlledAccountsResponse {
pub accounts: Vec<DelegatedAccountInfo>,
}
pub async fn list_controlled_accounts(
State(state): State<AppState>,
auth: Auth<Active>,
@@ -289,18 +288,7 @@ pub async fn list_controlled_accounts(
}
};
Ok(Json(ListControlledAccountsResponse {
accounts: accounts
.into_iter()
.map(|a| DelegatedAccountInfo {
did: a.did,
handle: a.handle,
granted_scopes: a.granted_scopes.into_string(),
granted_at: a.granted_at,
})
.collect(),
})
.into_response())
Ok(Json(serde_json::json!({ "accounts": accounts })).into_response())
}
#[derive(Debug, Deserialize)]
@@ -315,24 +303,6 @@ fn default_limit() -> i64 {
50
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditLogEntry {
pub id: String,
pub delegated_did: Did,
pub actor_did: Did,
pub controller_did: Option<Did>,
pub action_type: String,
pub action_details: Option<serde_json::Value>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct GetAuditLogResponse {
pub entries: Vec<AuditLogEntry>,
pub total: i64,
}
pub async fn get_audit_log(
State(state): State<AppState>,
auth: Auth<Active>,
@@ -361,50 +331,11 @@ pub async fn get_audit_log(
.await
.unwrap_or_default();
Ok(Json(GetAuditLogResponse {
entries: entries
.into_iter()
.map(|e| AuditLogEntry {
id: e.id.to_string(),
delegated_did: e.delegated_did,
actor_did: e.actor_did,
controller_did: e.controller_did,
action_type: format!("{:?}", e.action_type),
action_details: e.action_details,
created_at: e.created_at,
})
.collect(),
total,
})
.into_response())
}
#[derive(Debug, Serialize)]
pub struct ScopePresetInfo {
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub scopes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct GetScopePresetsResponse {
pub presets: Vec<ScopePresetInfo>,
Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response())
}
pub async fn get_scope_presets() -> Response {
Json(GetScopePresetsResponse {
presets: SCOPE_PRESETS
.iter()
.map(|p| ScopePresetInfo {
name: p.name,
label: p.label,
description: p.description,
scopes: p.scopes,
})
.collect(),
})
.into_response()
Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response()
}
#[derive(Debug, Deserialize)]
@@ -434,29 +365,11 @@ pub async fn create_delegated_account(
Err(response) => return Ok(response),
};
let hostname = &tranquil_config::get().server.hostname;
let available_domains = tranquil_config::get().server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.handle
.strip_suffix(&format!(".{}", domain))
.unwrap_or(&input.handle),
None => &input.handle,
};
match crate::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());
}
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
Ok(h) => h,
Err(e) => {
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
}
} else {
input.handle.to_lowercase()
};
let email = input
@@ -465,7 +378,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());
}
@@ -483,96 +396,15 @@ pub async fn create_delegated_account(
None
};
use k256::ecdsa::SigningKey;
use rand::rngs::OsRng;
let pds_endpoint = format!("https://{}", hostname);
let secret_key = k256::SecretKey::random(&mut OsRng);
let secret_key_bytes = secret_key.to_bytes().to_vec();
let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
Ok(k) => k,
Err(e) => {
error!("Error creating signing key: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
let genesis_result = match crate::plc::create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return Ok(
ApiError::InternalError(Some("Failed to create PLC operation".into()))
.into_response(),
);
}
};
let plc_client = crate::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
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return Ok(ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response());
}
let did: Did = genesis_result
.did
.parse()
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
let plc = create_plc_did(&state, &handle).await.map_err(|e| {
tracing::error!("PLC DID creation failed: {:?}", e);
e
})?;
let did = plc.did;
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) {
Ok(bytes) => bytes,
Err(e) => {
error!("Error encrypting signing key: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = match mst.persist().await {
Ok(c) => c,
Err(e) => {
error!("Error persisting MST: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let rev = Tid::now(LimitedU32::MIN);
let (commit_bytes, _sig) =
match create_signed_commit(&did, mst_root, rev.as_ref(), None, &signing_key) {
Ok(result) => result,
Err(e) => {
error!("Error creating genesis commit: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
Ok(c) => c,
Err(e) => {
error!("Error saving genesis commit: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?;
let create_input = tranquil_db_traits::CreateDelegatedAccountInput {
handle: handle.clone(),
@@ -580,11 +412,11 @@ pub async fn create_delegated_account(
did: did.clone(),
controller_did: can_control.did().clone(),
controller_scopes: input.controller_scopes.as_str().to_string(),
encrypted_key_bytes,
encryption_version: crate::config::ENCRYPTION_VERSION,
commit_cid: commit_cid.to_string(),
repo_rev: rev.as_ref().to_string(),
genesis_block_cids,
encrypted_key_bytes: repo.encrypted_key_bytes,
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
commit_cid: repo.commit_cid.to_string(),
repo_rev: repo.repo_rev.clone(),
genesis_block_cids: repo.genesis_block_cids,
invite_code: input.invite_code.clone(),
};
@@ -616,11 +448,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 +466,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
@@ -666,3 +498,40 @@ pub async fn create_delegated_account(
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
}
#[derive(Debug, Deserialize)]
pub struct ResolveControllerParams {
pub identifier: String,
}
pub async fn resolve_controller(
State(state): State<AppState>,
Query(params): Query<ResolveControllerParams>,
) -> Result<Response, ApiError> {
let identifier = params.identifier.trim().trim_start_matches('@');
let did: Did = if identifier.starts_with("did:") {
identifier.parse().map_err(|_| ApiError::ControllerNotFound)?
} else {
let local_handle: Option<Handle> = identifier.parse().ok();
let local_user = match local_handle {
Some(ref h) => state.user_repo.get_by_handle(h).await.ok().flatten(),
None => None,
};
match local_user {
Some(user) => user.did,
None => tranquil_pds::handle::resolve_handle(identifier)
.await
.map_err(|_| ApiError::ControllerNotFound)?
.parse()
.map_err(|_| ApiError::ControllerNotFound)?,
}
};
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did)
.await
.ok_or(ApiError::ControllerNotFound)?;
Ok(Json(resolved).into_response())
}
@@ -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 {
@@ -1,12 +1,10 @@
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::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
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,
@@ -14,13 +12,10 @@ use axum::{
response::{IntoResponse, Response},
};
use bcrypt::{DEFAULT_COST, hash};
use jacquard_common::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use k256::{SecretKey, ecdsa::SigningKey};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{debug, error, info, warn};
#[derive(Deserialize)]
@@ -73,7 +68,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) {
@@ -141,31 +136,9 @@ pub async fn create_account(
}
let cfg = tranquil_config::get();
let available_domains = cfg.server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
let validated_short_handle = if !input.handle.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.handle
.strip_suffix(&format!(".{}", domain))
.unwrap_or(&input.handle),
None => &input.handle,
};
match crate::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) {
Ok(h) => h,
Err(e) => return ApiError::from(e).into_response(),
}
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
Ok(h) => h,
Err(e) => return ApiError::from(e).into_response(),
};
let email: Option<String> = input
.email
@@ -173,7 +146,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 +164,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 +176,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();
@@ -221,12 +194,6 @@ pub async fn create_account(
})
};
let hostname = &cfg.server.hostname;
let pds_endpoint = format!("https://{}", hostname);
let handle = match matched_domain {
Some(domain) => format!("{}.{}", validated_short_handle, domain),
None if input.handle.contains('.') => validated_short_handle.clone(),
None => format!("{}.{}", validated_short_handle, &available_domains[0]),
};
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
if let Some(signing_key_did) = &input.signing_key {
match state
@@ -257,7 +224,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");
@@ -308,76 +275,17 @@ pub async fn create_account(
)
.into_response();
} else {
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
let genesis_result = match create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return ApiError::InternalError(Some(
"Failed to create PLC operation".into(),
))
.into_response();
}
};
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response();
Ok(did) => did,
Err(e) => return e.into_response(),
}
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
genesis_result.did
}
} else {
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
let genesis_result = match create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return ApiError::InternalError(Some(
"Failed to create PLC operation".into(),
))
.into_response();
}
};
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response();
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await {
Ok(did) => did,
Err(e) => return e.into_response(),
}
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
genesis_result.did
}
}
};
@@ -408,7 +316,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 +336,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,
) {
@@ -453,7 +361,7 @@ pub async fn create_account(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic transition:chat.bsky".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -463,12 +371,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,45 +498,23 @@ pub async fn create_account(
None
};
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
Ok(enc) => enc,
Err(e) => {
error!("Error encrypting user key: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = match mst.persist().await {
Ok(c) => c,
Err(e) => {
error!("Error persisting MST: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let rev = Tid::now(LimitedU32::MIN);
let did_for_commit: Did = match did.parse() {
Ok(d) => d,
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
};
let (commit_bytes, _sig) =
match create_signed_commit(&did_for_commit, mst_root, rev.as_ref(), None, &signing_key) {
Ok(result) => result,
Err(e) => {
error!("Error creating genesis commit: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let commit_cid = match state.block_store.put(&commit_bytes).await {
Ok(c) => c,
Err(e) => {
error!("Error saving genesis commit: {:?}", e);
return ApiError::InternalError(None).into_response();
}
let repo = match super::provision::init_genesis_repo(
&state,
&did_for_commit,
&signing_key,
&secret_key_bytes,
)
.await
{
Ok(r) => r,
Err(e) => return e.into_response(),
};
let commit_cid_str = commit_cid.to_string();
let rev_str = rev.as_ref().to_string();
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
let commit_cid_str = repo.commit_cid.to_string();
let rev_str = repo.repo_rev.clone();
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
Some(json!({
@@ -665,12 +551,12 @@ pub async fn create_account(
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase()),
deactivated_at,
encrypted_key_bytes,
encryption_version: crate::config::ENCRYPTION_VERSION,
encrypted_key_bytes: repo.encrypted_key_bytes,
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
reserved_key_id,
commit_cid: commit_cid_str.clone(),
repo_rev: rev_str.clone(),
genesis_block_cids,
genesis_block_cids: repo.genesis_block_cids,
invite_code: if is_bootstrap {
None
} else {
@@ -697,7 +583,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 +592,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,22 +601,22 @@ 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,
&mst_root,
&repo.commit_cid,
&repo.mst_root_cid,
&rev_str,
)
.await
{
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,
Some(rev.as_ref()),
Some(&rev_str),
)
.await
{
@@ -740,11 +626,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 +641,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 +667,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 +683,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 +692,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);
@@ -821,7 +707,7 @@ pub async fn create_account(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic transition:chat.bsky".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -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<AppState>, 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<AppState>, 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::<Vec<_>>(),
"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<u8> =
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<AppState>, Path(handle): Path<Stri
})).collect::<Vec<_>>(),
"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<AppState>, Path(handle): Path<Stri
Err(_) => return ApiError::InternalError(None).into_response(),
};
let key_bytes: Vec<u8> =
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<AppState>, Path(handle): Path<Stri
}],
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": service_endpoint
}]
}))
@@ -480,7 +480,7 @@ pub async fn verify_did_web(
let path = parts[3..].join("/");
format!("{}://{}/{}/did.json", scheme, domain, path)
};
let client = crate::api::proxy_client::did_resolution_client();
let client = tranquil_pds::api::proxy_client::did_resolution_client();
let resp = client
.get(&url)
.send()
@@ -503,7 +503,7 @@ pub async fn verify_did_web(
))?;
let pds_endpoint = format!("https://{}", hostname);
let has_valid_service = services.iter().any(|s| {
s["type"] == crate::plc::ServiceType::Pds.as_str() && s["serviceEndpoint"] == pds_endpoint
s["type"] == tranquil_pds::plc::ServiceType::Pds.as_str() && s["serviceEndpoint"] == pds_endpoint
});
if !has_valid_service {
return Err(DidWebVerifyError::PdsNotListed(pds_endpoint));
@@ -600,7 +600,7 @@ pub async fn get_recommended_did_credentials(
verification_methods: VerificationMethods { atproto: did_key },
services: Services {
atproto_pds: AtprotoPds {
service_type: crate::plc::ServiceType::Pds.as_str().to_string(),
service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
endpoint: pds_endpoint,
},
},
@@ -619,10 +619,10 @@ pub async fn update_handle(
auth: Auth<NotTakendown>,
Json(input): Json<UpdateHandleInput>,
) -> Result<Response, ApiError> {
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(&current_handle))
.delete(&tranquil_pds::cache_keys::handle_key(&current_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<dyn std::error::Error + Send + Sync>> {
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 = state.plc_client();
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<AppState>, 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(),
};
@@ -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()),
@@ -2,6 +2,7 @@ pub mod account;
pub mod did;
pub mod handle;
pub mod plc;
pub mod provision;
pub use account::create_account;
pub use did::{
@@ -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<AppState>,
auth: Auth<Permissive>,
) -> Result<Response, ApiError> {
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,
@@ -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::{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<Permissive>,
Json(input): Json<SignPlcOperationInput>,
) -> Result<Response, ApiError> {
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)
@@ -97,7 +97,7 @@ pub async fn sign_plc_operation(
ApiError::InternalError(None)
})?;
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
let plc_client = state.plc_client();
let did_clone = did.clone();
let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
plc_client.get_last_op(&did_clone).await
@@ -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::{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<Permissive>,
Json(input): Json<SubmitPlcOperationInput>,
) -> Result<Response, ApiError> {
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(),
));
@@ -120,7 +120,7 @@ pub async fn submit_plc_operation(
));
}
}
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
let plc_client = state.plc_client();
let operation_clone = input.operation.clone();
let did_clone = did.clone();
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
@@ -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");
@@ -0,0 +1,117 @@
use tranquil_pds::api::error::ApiError;
use tranquil_pds::repo_ops::create_signed_commit;
use tranquil_pds::state::AppState;
use tranquil_pds::types::Did;
use jacquard_common::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use k256::ecdsa::SigningKey;
use std::sync::Arc;
pub struct PlcDidResult {
pub did: Did,
pub signing_key_bytes: Vec<u8>,
pub signing_key: SigningKey,
}
pub async fn create_plc_did(state: &AppState, handle: &str) -> Result<PlcDidResult, ApiError> {
use k256::SecretKey;
use rand::rngs::OsRng;
let secret_key = SecretKey::random(&mut OsRng);
let secret_key_bytes = secret_key.to_bytes().to_vec();
let signing_key = SigningKey::from_slice(&secret_key_bytes).map_err(|e| {
tracing::error!("Error creating signing key: {:?}", e);
ApiError::InternalError(None)
})?;
let did_str = submit_plc_genesis(state, &signing_key, handle).await?;
let did: Did = did_str
.parse()
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
Ok(PlcDidResult {
did,
signing_key_bytes: secret_key_bytes,
signing_key,
})
}
pub async fn submit_plc_genesis(
state: &AppState,
signing_key: &SigningKey,
handle: &str,
) -> Result<String, ApiError> {
let hostname = &tranquil_config::get().server.hostname;
let pds_endpoint = format!("https://{}", hostname);
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(signing_key));
let genesis_result =
tranquil_pds::plc::create_genesis_operation(signing_key, &rotation_key, handle, &pds_endpoint)
.map_err(|e| {
tracing::error!("Error creating PLC genesis operation: {:?}", e);
ApiError::InternalError(Some("Failed to create PLC operation".into()))
})?;
state
.plc_client()
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
.map_err(|e| {
tracing::error!("Failed to submit PLC genesis operation: {:?}", e);
ApiError::UpstreamErrorMsg(format!("Failed to register DID with PLC directory: {}", e))
})?;
tracing::info!(did = %genesis_result.did, "Registered DID with PLC directory");
Ok(genesis_result.did)
}
pub struct GenesisRepo {
pub encrypted_key_bytes: Vec<u8>,
pub commit_cid: cid::Cid,
pub mst_root_cid: cid::Cid,
pub repo_rev: String,
pub genesis_block_cids: Vec<Vec<u8>>,
}
pub async fn init_genesis_repo(
state: &AppState,
did: &Did,
signing_key: &SigningKey,
signing_key_bytes: &[u8],
) -> Result<GenesisRepo, ApiError> {
let encrypted_key_bytes = tranquil_pds::config::encrypt_key(signing_key_bytes).map_err(|e| {
tracing::error!("Error encrypting signing key: {:?}", e);
ApiError::InternalError(None)
})?;
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = mst.persist().await.map_err(|e| {
tracing::error!("Error persisting MST: {:?}", e);
ApiError::InternalError(None)
})?;
let rev = Tid::now(LimitedU32::MIN);
let (commit_bytes, _sig) = create_signed_commit(did, mst_root, rev.as_ref(), None, signing_key)
.map_err(|e| {
tracing::error!("Error creating genesis commit: {:?}", e);
ApiError::InternalError(None)
})?;
let commit_cid: cid::Cid = state.block_store.put(&commit_bytes).await.map_err(|e| {
tracing::error!("Error saving genesis commit: {:?}", e);
ApiError::InternalError(None)
})?;
Ok(GenesisRepo {
encrypted_key_bytes,
commit_cid,
mst_root_cid: mst_root,
repo_rev: rev.as_ref().to_string(),
genesis_block_cids: vec![mst_root.to_bytes(), commit_cid.to_bytes()],
})
}
+274
View File
@@ -0,0 +1,274 @@
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<AppState> {
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("/_delegation.resolveController", get(delegation::resolve_controller))
.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<AppState> {
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<AppState> {
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<AppState> {
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))
}
@@ -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 {
@@ -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<String, ApiError> {
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(),
@@ -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,
@@ -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)
@@ -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();
@@ -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()
})?;
@@ -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<AppState>,
auth: Auth<Active>,
Json(input): Json<DeleteRecordInput>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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(
@@ -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::<crate::types::Rkey>().ok());
.and_then(|c| c.parse::<tranquil_pds::types::Rkey>().ok());
let rows = match state
.repo_repo
.list_records(
@@ -0,0 +1 @@
pub use tranquil_pds::repo_ops::*;
@@ -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(
@@ -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<AppState>,
auth: Auth<Active>,
Json(input): Json<CreateRecordInput>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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<AppState>,
auth: Auth<Active>,
Json(input): Json<PutRecordInput>,
) -> Result<Response, crate::api::error::ApiError> {
) -> Result<Response, tranquil_pds::api::error::ApiError> {
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());
@@ -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<dyn Cache>,
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<dyn Cache>,
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<Permissive>,
Json(input): Json<DeactivateAccountInput>,
) -> Result<Response, ApiError> {
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()
@@ -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;
@@ -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<NotTakendown>,
input: Option<Json<RequestEmailUpdateInput>>,
) -> Result<Response, ApiError> {
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<NotTakendown>,
Json(input): Json<ConfirmEmailInput>,
) -> Result<Response, ApiError> {
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<NotTakendown>,
Json(input): Json<UpdateEmailInput>,
) -> Result<Response, ApiError> {
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,
&current_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<VerificationCheckLimit>,
axum::extract::Query(query): axum::extract::Query<AuthorizeEmailUpdateQuery>,
) -> 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<VerificationCheckLimit>,
auth: Auth<NotTakendown>,
) -> Result<Response, ApiError> {
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);
}
@@ -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,
@@ -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<AppState>) -> 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(),
};
@@ -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;
@@ -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::<Vec<_>>(),
"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
}]
})
@@ -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<CreatePasskeyAccountInput>,
) -> 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) {
@@ -114,31 +114,9 @@ pub async fn create_passkey_account(
let cfg = tranquil_config::get();
let hostname = &cfg.server.hostname;
let available_domains = cfg.server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.handle
.strip_suffix(&format!(".{}", domain))
.unwrap_or(&input.handle),
None => &input.handle,
};
match crate::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) {
Ok(h) => h,
Err(_) => return ApiError::InvalidHandle(None).into_response(),
}
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
Ok(h) => h,
Err(_) => return ApiError::InvalidHandle(None).into_response(),
};
let email = input
@@ -147,7 +125,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 +162,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 +174,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 +228,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 +262,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 +306,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 +324,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 +359,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 +436,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 +465,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 +474,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 +487,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 +500,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 +524,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);
@@ -558,7 +536,7 @@ pub async fn create_passkey_account(
refresh_expires_at: refresh_expires,
login_type: tranquil_db::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -887,7 +865,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 +946,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)
@@ -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)
}
@@ -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<Active>,
Json(input): Json<ChangePasswordInput>,
) -> Result<Response, ApiError> {
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,
@@ -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<dyn crate::cache::Cache>,
did: &crate::types::Did,
cache: &std::sync::Arc<dyn tranquil_pds::cache::Cache>,
did: &tranquil_pds::types::Did,
) -> Result<DateTime<Utc>, 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<DateTime<Utc>>) -> 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<ReauthMethod> {
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<dyn crate::cache::Cache>,
did: &crate::types::Did,
cache: &std::sync::Arc<dyn tranquil_pds::cache::Cache>,
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::<i64>()
{
@@ -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;
(
@@ -1,8 +1,7 @@
use crate::AccountStatus;
use crate::api::error::ApiError;
use crate::state::AppState;
use crate::types::Did;
use axum::http::Method;
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},
@@ -10,7 +9,6 @@ use axum::{
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashSet;
use std::sync::LazyLock;
use tracing::{error, info, warn};
@@ -59,112 +57,25 @@ pub struct GetServiceAuthOutput {
pub async fn get_service_auth(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
auth: Auth<Permissive>,
Query(params): Query<GetServiceAuthParams>,
) -> Response {
let auth_header = crate::util::get_header_str(&headers, axum::http::header::AUTHORIZATION);
let dpop_proof = crate::util::get_header_str(&headers, crate::util::HEADER_DPOP);
info!(
has_auth_header = auth_header.is_some(),
has_dpop_proof = dpop_proof.is_some(),
did = %&auth.did,
is_oauth = auth.is_oauth(),
aud = %params.aud,
lxm = ?params.lxm,
"getServiceAuth called"
);
let auth_header = match auth_header {
Some(h) => h.trim(),
None => {
warn!("getServiceAuth: no Authorization header");
return ApiError::AuthenticationRequired.into_response();
}
};
let extracted = match crate::auth::extract_auth_token_from_header(Some(auth_header)) {
Some(e) => e,
None => {
warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme");
return ApiError::AuthenticationRequired.into_response();
}
};
let token = extracted.token;
let auth_user = if extracted.scheme.is_dpop() {
match crate::oauth::verify::verify_oauth_access_token(
state.oauth_repo.as_ref(),
&token,
dpop_proof,
Method::GET.as_str(),
&crate::util::build_full_url(&format!(
"/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}",
params.aud,
params.lxm.as_ref().map_or("", |n| n.as_str())
)),
)
.await
{
Ok(result) => {
let did: Did = match result.did.parse() {
Ok(d) => d,
Err(_) => {
return ApiError::InternalError(Some("Invalid DID in token".into()))
.into_response();
}
};
crate::auth::AuthenticatedUser {
did,
is_admin: false,
status: AccountStatus::Active,
scope: result.scope,
key_bytes: None,
controller_did: None,
auth_source: crate::auth::AuthSource::OAuth,
}
}
Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => {
return (
StatusCode::UNAUTHORIZED,
[("DPoP-Nonce", nonce)],
Json(json!({
"error": "use_dpop_nonce",
"message": "DPoP nonce required"
})),
)
.into_response();
}
Err(crate::oauth::OAuthError::ExpiredToken(msg)) => {
warn!(error = %msg, "getServiceAuth DPoP token expired");
return ApiError::OAuthExpiredToken(Some(msg)).into_response();
}
Err(e) => {
warn!(error = ?e, "getServiceAuth DPoP auth validation failed");
return ApiError::AuthenticationFailed(Some(format!("{:?}", e))).into_response();
}
}
} else {
match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token)
.await
{
Ok(user) => user,
Err(e) => {
warn!(error = ?e, "getServiceAuth auth validation failed");
return ApiError::from(e).into_response();
}
}
};
info!(
did = %&auth_user.did,
is_oauth = auth_user.is_oauth(),
has_key = auth_user.key_bytes.is_some(),
"getServiceAuth auth validated"
);
let key_bytes = match &auth_user.key_bytes {
let key_bytes = match &auth.key_bytes {
Some(kb) => kb.clone(),
None => {
warn!(did = %&auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB");
match state.user_repo.get_user_info_by_did(&auth_user.did).await {
warn!(did = %&auth.did, "getServiceAuth: no key_bytes in auth, fetching from DB");
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");
@@ -201,16 +112,16 @@ 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(
&auth_user.auth_source,
auth_user.scope.as_deref(),
if let Err(e) = tranquil_pds::auth::scope_check::check_rpc_scope(
&auth.auth_source,
auth.scope.as_deref(),
params.aud.as_str(),
method.as_str(),
) {
return e;
}
} else if auth_user.is_oauth() {
let permissions = auth_user.permissions();
} else if auth.is_oauth() {
let permissions = auth.permissions();
if !permissions.has_full_access() {
return ApiError::InvalidRequest(
"OAuth tokens with granular scopes must specify an lxm parameter".into(),
@@ -219,15 +130,7 @@ pub async fn get_service_auth(
}
}
let is_takendown = state
.user_repo
.get_status_by_did(&auth_user.did)
.await
.ok()
.flatten()
.is_some_and(|s| s.takedown_ref.is_some());
if is_takendown && lxm != Some(&*CREATE_ACCOUNT_NSID) {
if auth.status.is_takendown() && lxm != Some(&*CREATE_ACCOUNT_NSID) {
return ApiError::InvalidToken(Some("Bad token scope".into())).into_response();
}
@@ -264,8 +167,8 @@ pub async fn get_service_auth(
}
}
let service_token = match crate::auth::create_service_token(
&auth_user.did,
let service_token = match tranquil_pds::auth::create_service_token(
&auth.did,
params.aud.as_str(),
lxm_for_token,
&key_bytes,
@@ -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,
@@ -68,7 +68,7 @@ pub async fn create_session(
let pds_host = &tranquil_config::get().server.hostname;
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let normalized_identifier =
NormalizedLoginIdentifier::normalize(&input.identifier, &hostname_for_handles);
NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles);
info!(
"Normalized identifier: {} -> {}",
input.identifier, normalized_identifier
@@ -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<Active>,
) -> Result<Response, ApiError> {
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<RefreshSessionLimit>,
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);
@@ -708,7 +708,7 @@ pub async fn confirm_signup(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic transition:chat.bsky".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -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<AutoResendResult> {
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<Aut
return Some(result);
}
let verification_token =
crate::auth::verification_token::generate_signup_token(did, row.channel, &recipient);
tranquil_pds::auth::verification_token::generate_signup_token(did, row.channel, &recipient);
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(),
row.id,
@@ -856,12 +856,12 @@ pub async fn resend_verification(
};
let verification_token =
crate::auth::verification_token::generate_signup_token(&input.did, row.channel, &recipient);
tranquil_pds::auth::verification_token::generate_signup_token(&input.did, row.channel, &recipient);
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(),
row.id,
@@ -910,7 +910,7 @@ pub async fn list_sessions(
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.and_then(|token| crate::auth::get_jti_from_token(token).ok());
.and_then(|token| tranquil_pds::auth::get_jti_from_token(token).ok());
let jwt_rows = state
.session_repo
@@ -990,7 +990,7 @@ pub async fn revoke_session(
.delete_session_by_id(session_id)
.await
.log_db_err("deleting session")?;
let cache_key = crate::cache_keys::session_key(&auth.did, &access_jti);
let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, &access_jti);
if let Err(e) = state.cache.delete(&cache_key).await {
warn!("Failed to invalidate session cache: {:?}", e);
}
@@ -1020,10 +1020,10 @@ pub async fn revoke_all_sessions(
headers: HeaderMap,
auth: Auth<Active>,
) -> Result<Response, ApiError> {
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")]
@@ -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<crate::types::Did>,
pub did: Option<tranquil_pds::types::Did>,
}
#[derive(Serialize)]
@@ -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)
}
@@ -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;
@@ -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,
@@ -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);
}
@@ -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 {
@@ -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()) {
@@ -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<AppState>,
Json(input): Json<ConfirmChannelVerificationInput>,
) -> 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(),
}
+4 -6
View File
@@ -327,12 +327,10 @@ impl TranquilConfig {
errors: &mut Vec<String>,
) {
self.validate_sso_provider(prefix, p, errors);
if p.get_enabled() {
if p.get_issuer().is_none() {
errors.push(format!(
"{prefix}.issuer is required when {prefix}.enabled = true"
));
}
if p.get_enabled() && p.get_issuer().is_none() {
errors.push(format!(
"{prefix}.issuer is required when {prefix}.enabled = true"
));
}
}
+7 -15
View File
@@ -20,6 +20,7 @@ pub struct DelegationGrant {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegatedAccountInfo {
pub did: Did,
pub handle: Handle,
@@ -28,12 +29,14 @@ pub struct DelegatedAccountInfo {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerInfo {
pub did: Did,
pub handle: Handle,
pub handle: Option<Handle>,
pub granted_scopes: DbScope,
pub granted_at: DateTime<Utc>,
pub is_active: bool,
pub is_local: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -48,6 +51,7 @@ pub enum DelegationActionType {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditLogEntry {
pub id: Uuid,
pub delegated_did: Did,
@@ -55,7 +59,9 @@ pub struct AuditLogEntry {
pub controller_did: Option<Did>,
pub action_type: DelegationActionType,
pub action_details: Option<serde_json::Value>,
#[serde(skip_serializing)]
pub ip_address: Option<String>,
#[serde(skip_serializing)]
pub user_agent: Option<String>,
pub created_at: DateTime<Utc>,
}
@@ -102,15 +108,8 @@ pub trait DelegationRepository: Send + Sync {
controller_did: &Did,
) -> Result<Vec<DelegatedAccountInfo>, DbError>;
async fn get_active_controllers_for_account(
&self,
delegated_did: &Did,
) -> Result<Vec<ControllerInfo>, DbError>;
async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError>;
async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError>;
async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError>;
#[allow(clippy::too_many_arguments)]
@@ -132,12 +131,5 @@ pub trait DelegationRepository: Send + Sync {
offset: i64,
) -> Result<Vec<AuditLogEntry>, DbError>;
async fn get_audit_log_by_controller(
&self,
controller_did: &Did,
limit: i64,
offset: i64,
) -> Result<Vec<AuditLogEntry>, DbError>;
async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError>;
}
+13 -109
View File
@@ -185,13 +185,17 @@ impl DelegationRepository for PostgresDelegationRepository {
let rows = sqlx::query!(
r#"
SELECT
u.did,
u.handle,
d.controller_did,
u.handle as "handle?",
d.granted_scopes,
d.granted_at,
(u.deactivated_at IS NULL AND u.takedown_ref IS NULL) as "is_active!"
CASE WHEN u.did IS NOT NULL
THEN u.deactivated_at IS NULL AND u.takedown_ref IS NULL
ELSE true
END as "is_active!",
u.did IS NOT NULL as "is_local!"
FROM account_delegations d
JOIN users u ON u.did = d.controller_did
LEFT JOIN users u ON u.did = d.controller_did
WHERE d.delegated_did = $1 AND d.revoked_at IS NULL
ORDER BY d.granted_at DESC
"#,
@@ -204,11 +208,12 @@ impl DelegationRepository for PostgresDelegationRepository {
Ok(rows
.into_iter()
.map(|r| ControllerInfo {
did: r.did.into(),
handle: r.handle.into(),
did: r.controller_did.into(),
handle: r.handle.map(Into::into),
granted_scopes: DbScope::from_db(r.granted_scopes),
granted_at: r.granted_at,
is_active: r.is_active,
is_local: r.is_local,
})
.collect())
}
@@ -249,54 +254,15 @@ impl DelegationRepository for PostgresDelegationRepository {
.collect())
}
async fn get_active_controllers_for_account(
&self,
delegated_did: &Did,
) -> Result<Vec<ControllerInfo>, DbError> {
let rows = sqlx::query!(
r#"
SELECT
u.did,
u.handle,
d.granted_scopes,
d.granted_at,
true as "is_active!"
FROM account_delegations d
JOIN users u ON u.did = d.controller_did
WHERE d.delegated_did = $1
AND d.revoked_at IS NULL
AND u.deactivated_at IS NULL
AND u.takedown_ref IS NULL
ORDER BY d.granted_at DESC
"#,
delegated_did.as_str()
)
.fetch_all(&self.pool)
.await
.map_err(map_sqlx_error)?;
Ok(rows
.into_iter()
.map(|r| ControllerInfo {
did: r.did.into(),
handle: r.handle.into(),
granted_scopes: DbScope::from_db(r.granted_scopes),
granted_at: r.granted_at,
is_active: r.is_active,
})
.collect())
}
async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError> {
let count = sqlx::query_scalar!(
r#"
SELECT COUNT(*) as "count!"
FROM account_delegations d
JOIN users u ON u.did = d.controller_did
LEFT JOIN users u ON u.did = d.controller_did
WHERE d.delegated_did = $1
AND d.revoked_at IS NULL
AND u.deactivated_at IS NULL
AND u.takedown_ref IS NULL
AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))
"#,
delegated_did.as_str()
)
@@ -307,21 +273,6 @@ impl DelegationRepository for PostgresDelegationRepository {
Ok(count)
}
async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError> {
let exists = sqlx::query_scalar!(
r#"SELECT EXISTS(
SELECT 1 FROM account_delegations
WHERE delegated_did = $1 AND revoked_at IS NULL
) as "exists!""#,
did.as_str()
)
.fetch_one(&self.pool)
.await
.map_err(map_sqlx_error)?;
Ok(exists)
}
async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError> {
let exists = sqlx::query_scalar!(
r#"SELECT EXISTS(
@@ -418,53 +369,6 @@ impl DelegationRepository for PostgresDelegationRepository {
.collect())
}
async fn get_audit_log_by_controller(
&self,
controller_did: &Did,
limit: i64,
offset: i64,
) -> Result<Vec<AuditLogEntry>, DbError> {
let rows = sqlx::query!(
r#"
SELECT
id,
delegated_did,
actor_did,
controller_did,
action_type as "action_type: PgDelegationActionType",
action_details,
ip_address,
user_agent,
created_at
FROM delegation_audit_log
WHERE controller_did = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
"#,
controller_did.as_str(),
limit,
offset
)
.fetch_all(&self.pool)
.await
.map_err(map_sqlx_error)?;
Ok(rows
.into_iter()
.map(|r| AuditLogEntry {
id: r.id,
delegated_did: r.delegated_did.into(),
actor_did: r.actor_did.into(),
controller_did: r.controller_did.map(Into::into),
action_type: r.action_type.into(),
action_details: r.action_details,
ip_address: r.ip_address,
user_agent: r.user_agent,
created_at: r.created_at,
})
.collect())
}
async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError> {
let count = sqlx::query_scalar!(
r#"SELECT COUNT(*) as "count!" FROM delegation_audit_log WHERE delegated_did = $1"#,
+1 -3
View File
@@ -46,9 +46,7 @@ pub fn is_valid_uri(s: &str) -> bool {
}
pub fn is_valid_cid(s: &str) -> bool {
s.len() >= 8
&& s.chars().all(|c| c.is_ascii_alphanumeric())
&& s.starts_with(|c: char| c == 'b' || c == 'z' || c == 'Q')
s.len() >= 8 && s.chars().all(|c| c.is_ascii_alphanumeric()) && s.starts_with(['b', 'z', 'Q'])
}
pub fn is_valid_language(s: &str) -> bool {
+8 -9
View File
@@ -339,15 +339,14 @@ fn validate_blob_ref(
}
}
if let Some(max_size) = lex_blob.max_size {
if let Some(size) = obj.get("size").and_then(|v| v.as_u64()) {
if size > max_size {
return Err(LexValidationError::field(
path,
format!("blob size {} exceeds max_size {}", size, max_size),
));
}
}
if let (Some(max_size), Some(size)) =
(lex_blob.max_size, obj.get("size").and_then(|v| v.as_u64()))
&& size > max_size
{
return Err(LexValidationError::field(
path,
format!("blob size {} exceeds max_size {}", size, max_size),
));
}
Ok(())
+35
View File
@@ -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 }
@@ -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<tranquil_types::DeviceId
cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| {
cookie
.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME))
.and_then(|value| crate::config::AuthConfig::get().verify_device_cookie(value))
.and_then(|value| tranquil_pds::config::AuthConfig::get().verify_device_cookie(value))
.map(tranquil_types::DeviceId::new)
})
})
@@ -109,7 +109,7 @@ fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
}
fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String {
let signed_value = crate::config::AuthConfig::get().sign_device_cookie(device_id.as_str());
let signed_value = tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str());
format!(
"{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
DEVICE_COOKIE_NAME, signed_value
@@ -256,7 +256,7 @@ pub async fn authorize_get(
if let Some(ref login_hint) = request_data.parameters.login_hint {
tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation");
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let normalized = NormalizedLoginIdentifier::normalize(login_hint, &hostname_for_handles);
let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles);
tracing::info!(normalized = %normalized, "Normalized login_hint");
match state
@@ -343,7 +343,7 @@ pub async fn authorize_get_json(
.oauth_repo
.get_authorization_request(&request_id_json)
.await
.map_err(crate::oauth::db_err_to_oauth)?
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
.ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
if request_data.expires_at < Utc::now() {
let _ = state
@@ -530,7 +530,7 @@ pub async fn authorize_post(
};
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let normalized_username =
NormalizedLoginIdentifier::normalize(&form.username, &hostname_for_handles);
NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles);
tracing::debug!(
original_username = %form.username,
normalized_username = %normalized_username,
@@ -626,7 +626,7 @@ pub async fn authorize_post(
}
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;
let handle = resend_info
.as_ref()
.map(|r| r.handle.to_string())
@@ -653,11 +653,11 @@ pub async fn authorize_post(
url_encode("account_not_verified")
));
}
let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await;
let has_totp = tranquil_api::server::has_totp_enabled(&state, &user.did).await;
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, &user.did)
tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did)
.await
} else {
false
@@ -665,7 +665,7 @@ pub async fn authorize_post(
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 {
@@ -978,7 +978,7 @@ pub async fn authorize_select(
};
let is_verified = user.channel_verification.has_any_verified();
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!({
@@ -991,11 +991,11 @@ pub async fn authorize_select(
)
.into_response();
}
let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await;
let select_early_device_typed = device_id.clone();
if has_totp {
let device_is_trusted =
crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did)
tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did)
.await;
if !device_is_trusted {
if state
@@ -1016,7 +1016,7 @@ pub async fn authorize_select(
.into_response();
}
let _ =
crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await;
tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await;
}
if user.two_factor_enabled {
let _ = state
@@ -1413,7 +1413,7 @@ pub async fn consent_get(
};
let effective_scope_str = if let Some(ref grant) = delegation_grant {
crate::delegation::intersect_scopes(requested_scope_str, grant.granted_scopes.as_str())
tranquil_pds::delegation::intersect_scopes(requested_scope_str, grant.granted_scopes.as_str())
} else {
requested_scope_str.to_string()
};
@@ -1445,7 +1445,7 @@ pub async fn consent_get(
.iter()
.map(|scope| {
let (category, required, description, display_name) = if let Some(def) =
crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope)
tranquil_pds::oauth::scopes::SCOPE_DEFINITIONS.get(*scope)
{
let desc = if *scope == "atproto" && has_granular_scopes {
"AT Protocol baseline scope (permissions determined by selected options below)"
@@ -1510,7 +1510,7 @@ pub async fn consent_get(
.map(|h| h.to_string());
let level = if let Some(ref grant) = delegation_grant {
let preset = crate::delegation::SCOPE_PRESETS
let preset = tranquil_pds::delegation::SCOPE_PRESETS
.iter()
.find(|p| p.scopes == grant.granted_scopes.as_str());
preset
@@ -1622,7 +1622,7 @@ pub async fn consent_post(
};
let effective_scope_str = if let Some(ref grant) = delegation_grant {
crate::delegation::intersect_scopes(original_scope_str, grant.granted_scopes.as_str())
tranquil_pds::delegation::intersect_scopes(original_scope_str, grant.granted_scopes.as_str())
} else {
original_scope_str.to_string()
};
@@ -1955,7 +1955,7 @@ pub async fn authorize_2fa_post(
);
}
};
if !crate::api::server::has_totp_enabled(&state, &did).await {
if !tranquil_api::server::has_totp_enabled(&state, &did).await {
return json_error(
StatusCode::BAD_REQUEST,
"invalid_request",
@@ -1973,7 +1973,7 @@ pub async fn authorize_2fa_post(
}
};
let totp_valid =
crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
tranquil_api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
if !totp_valid {
return json_error(
StatusCode::FORBIDDEN,
@@ -2011,7 +2011,7 @@ pub async fn authorize_2fa_post(
.oauth_repo
.upsert_account_device(&did, &trust_device_id)
.await;
let _ = crate::api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await;
let _ = tranquil_api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await;
}
let requested_scope_str = request_data
.parameters
@@ -2102,7 +2102,7 @@ pub async fn check_user_has_passkeys(
) -> Response {
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let bare_identifier =
BareLoginIdentifier::from_identifier(&query.identifier, &hostname_for_handles);
BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles);
let user = state
.user_repo
@@ -2110,7 +2110,7 @@ pub async fn check_user_has_passkeys(
.await;
let has_passkeys = match user {
Ok(Some(u)) => 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,
};
@@ -2134,7 +2134,7 @@ pub async fn check_user_security_status(
) -> Response {
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let normalized_identifier =
NormalizedLoginIdentifier::normalize(&query.identifier, &hostname_for_handles);
NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles);
let user = state
.user_repo
@@ -2149,8 +2149,8 @@ pub async fn check_user_security_status(
Option<String>,
) = 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
@@ -2242,7 +2242,7 @@ pub async fn passkey_start(
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
let normalized_username =
NormalizedLoginIdentifier::normalize(&form.identifier, &hostname_for_handles);
NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles);
let user = match state
.user_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<AppState>,
headers: HeaderMap,
auth: crate::auth::Auth<crate::auth::Active>,
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
) -> Response {
let did = &auth.did;
@@ -0,0 +1,584 @@
use tranquil_pds::auth::{Active, Auth};
use tranquil_pds::delegation::DelegationActionType;
use tranquil_pds::oauth::client::{build_client_metadata, delegation_oauth_urls};
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::{Query, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
use serde::{Deserialize, Serialize};
use tranquil_pds::oauth::RequestData;
use tranquil_types::did_doc::{extract_handle, extract_pds_endpoint};
use tranquil_types::{Did, RequestId};
#[allow(clippy::result_large_err)]
fn parse_did(s: &str, label: &str) -> Result<Did, Response> {
s.parse()
.map_err(|_| DelegationAuthResponse::err(format!("Invalid {} DID", label)))
}
async fn get_auth_request(state: &AppState, request_uri: &str) -> Result<RequestData, Response> {
let request_id = RequestId::from(request_uri.to_string());
match state
.oauth_repo
.get_authorization_request(&request_id)
.await
{
Ok(Some(r)) => Ok(r),
Ok(None) => Err(DelegationAuthResponse::err(
"Authorization request not found",
)),
Err(_) => Err(DelegationAuthResponse::err("Server error")),
}
}
async fn get_delegation_grant(
state: &AppState,
delegated_did: &Did,
controller_did: &Did,
) -> Result<tranquil_db_traits::DelegationGrant, Response> {
match state
.delegation_repo
.get_delegation(delegated_did, controller_did)
.await
{
Ok(Some(g)) => Ok(g),
Ok(None) => Err(DelegationAuthResponse::err(
"No delegation grant found for this controller",
)),
Err(_) => Err(DelegationAuthResponse::err("Server error")),
}
}
async fn finalize_delegation_auth(
state: &AppState,
request_uri: &str,
delegated_did: &Did,
controller_did: &Did,
details: serde_json::Value,
ip: Option<&str>,
user_agent: Option<&str>,
) -> Response {
let _ = state
.delegation_repo
.log_delegation_action(
delegated_did,
controller_did,
Some(controller_did),
DelegationActionType::TokenIssued,
Some(details),
ip,
user_agent,
)
.await;
consent_redirect(request_uri)
}
async fn bind_delegation_to_request(
state: &AppState,
request_uri: &str,
delegated_did: &Did,
controller_did: &Did,
) -> Result<(), Response> {
let request_id = RequestId::from(request_uri.to_string());
state
.oauth_repo
.set_request_did(&request_id, delegated_did)
.await
.map_err(|_| DelegationAuthResponse::err("Failed to update authorization request"))?;
state
.oauth_repo
.set_controller_did(&request_id, controller_did)
.await
.map_err(|_| DelegationAuthResponse::err("Failed to update authorization request"))?;
Ok(())
}
fn consent_url(request_uri: &str) -> String {
format!(
"/app/oauth/consent?request_uri={}",
urlencoding::encode(request_uri)
)
}
fn consent_redirect(request_uri: &str) -> Response {
DelegationAuthResponse::redirect(consent_url(request_uri))
}
#[derive(Debug, Deserialize)]
pub struct DelegationAuthSubmit {
pub request_uri: String,
pub delegated_did: Option<String>,
pub controller_did: String,
pub password: Option<PlainPassword>,
#[serde(default)]
pub remember_device: bool,
pub auth_method: Option<String>,
}
enum DelegationAuthResponse {
Redirect(String),
NeedsTotp(String),
Error(String),
TotpError(String),
}
impl DelegationAuthResponse {
fn err(msg: impl Into<String>) -> Response {
Self::Error(msg.into()).into_response()
}
fn redirect(uri: impl Into<String>) -> Response {
Self::Redirect(uri.into()).into_response()
}
fn needs_totp(uri: impl Into<String>) -> Response {
Self::NeedsTotp(uri.into()).into_response()
}
fn totp_error(msg: impl Into<String>) -> Response {
Self::TotpError(msg.into()).into_response()
}
}
impl IntoResponse for DelegationAuthResponse {
fn into_response(self) -> Response {
let (success, needs_totp, redirect_uri, error) = match self {
Self::Redirect(uri) => (true, None, Some(uri), None),
Self::NeedsTotp(uri) => (true, Some(true), Some(uri), None),
Self::Error(msg) => (false, None, None, Some(msg)),
Self::TotpError(msg) => (false, Some(true), None, Some(msg)),
};
#[derive(Serialize)]
struct Body {
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
needs_totp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
redirect_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
Json(Body {
success,
needs_totp,
redirect_uri,
error,
})
.into_response()
}
}
pub async fn delegation_auth(
State(state): State<AppState>,
rate_limit: OAuthRateLimited<LoginLimit>,
headers: HeaderMap,
Json(form): Json<DelegationAuthSubmit>,
) -> Response {
let client_ip = rate_limit.client_ip();
let request = match get_auth_request(&state, &form.request_uri).await {
Ok(r) => r,
Err(resp) => return resp,
};
let delegated_did = if let Some(did_str) = form.delegated_did.as_ref() {
match parse_did(did_str, "delegated") {
Ok(d) => d,
Err(resp) => return resp,
}
} else if let Some(did) = request.did.clone() {
did
} else {
return DelegationAuthResponse::err("No delegated account selected");
};
let controller_did = match parse_did(&form.controller_did, "controller") {
Ok(d) => d,
Err(resp) => return resp,
};
let grant = match get_delegation_grant(&state, &delegated_did, &controller_did).await {
Ok(g) => g,
Err(resp) => return resp,
};
let is_cross_pds = form.auth_method.as_deref() == Some("cross_pds");
let controller_local = state
.user_repo
.get_auth_info_by_did(&controller_did)
.await
.ok()
.flatten();
if is_cross_pds || controller_local.is_none() {
let did_doc = match state
.plc_client()
.get_document(controller_did.as_str())
.await
{
Ok(doc) => doc,
Err(_) => {
return DelegationAuthResponse::err("Failed to resolve controller DID");
}
};
let pds_url = match extract_pds_endpoint(&did_doc) {
Some(url) => url,
None => {
return DelegationAuthResponse::err("Controller has no PDS endpoint");
}
};
let hostname = &tranquil_config::get().server.hostname;
let urls = delegation_oauth_urls(hostname);
let login_hint = extract_handle(&did_doc);
let (par_result, auth_state, oauth_state) = match state
.cross_pds_oauth
.initiate_par(
&pds_url,
&urls,
login_hint.as_deref(),
&form.request_uri,
&controller_did,
&delegated_did,
)
.await
{
Ok(result) => result,
Err(e) => {
tracing::error!("Cross-PDS PAR failed: {:?}", e);
return DelegationAuthResponse::err("Failed to initiate cross-PDS authentication");
}
};
if let Err(e) = state
.cross_pds_oauth
.store_auth_state(&oauth_state, &auth_state)
.await
{
tracing::error!("Failed to store cross-PDS auth state: {:?}", e);
return DelegationAuthResponse::err(
"Internal error preparing cross-PDS authentication",
);
}
return DelegationAuthResponse::redirect(par_result.authorize_url);
}
let controller = controller_local.unwrap();
if controller.deactivated_at.is_some() {
return DelegationAuthResponse::err("Controller account is deactivated");
}
if controller.takedown_ref.is_some() {
return DelegationAuthResponse::err("Controller account has been taken down");
}
let password = match form.password {
Some(ref pw) => pw,
None => {
return DelegationAuthResponse::err("Password required for local controller");
}
};
let password_valid = controller
.password_hash
.as_ref()
.map(|hash| bcrypt::verify(password, hash).unwrap_or_default())
.unwrap_or_default();
if !password_valid {
return DelegationAuthResponse::err("Invalid password");
}
if let Err(resp) =
bind_delegation_to_request(&state, &form.request_uri, &delegated_did, &controller_did).await
{
return resp;
}
let has_totp = tranquil_api::server::has_totp_enabled(&state, &controller_did).await;
if has_totp {
return DelegationAuthResponse::needs_totp(format!(
"/app/oauth/delegation-totp?request_uri={}",
urlencoding::encode(&form.request_uri)
));
}
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
finalize_delegation_auth(
&state,
&form.request_uri,
&delegated_did,
&controller_did,
serde_json::json!({
"client_id": request.client_id,
"granted_scopes": grant.granted_scopes
}),
Some(client_ip),
user_agent.as_deref(),
)
.await
}
#[derive(Debug, Deserialize)]
pub struct DelegationTotpSubmit {
pub request_uri: String,
pub code: String,
}
pub async fn delegation_totp_verify(
State(state): State<AppState>,
rate_limit: OAuthRateLimited<TotpVerifyLimit>,
headers: HeaderMap,
Json(form): Json<DelegationTotpSubmit>,
) -> Response {
let client_ip = rate_limit.client_ip();
let request = match get_auth_request(&state, &form.request_uri).await {
Ok(r) => r,
Err(resp) => return resp,
};
let controller_did = match request.controller_did {
Some(did) => did,
None => return DelegationAuthResponse::err("Controller not authenticated"),
};
let delegated_did = match request.did {
Some(did) => did,
None => return DelegationAuthResponse::err("No delegated account"),
};
let grant = match get_delegation_grant(&state, &delegated_did, &controller_did).await {
Ok(g) => g,
Err(resp) => return resp,
};
let totp_valid =
tranquil_api::server::verify_totp_or_backup_for_user(&state, &controller_did, &form.code)
.await;
if !totp_valid {
return DelegationAuthResponse::totp_error("Invalid TOTP code");
}
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
finalize_delegation_auth(
&state,
&form.request_uri,
&delegated_did,
&controller_did,
serde_json::json!({
"client_id": request.client_id,
"granted_scopes": grant.granted_scopes
}),
Some(client_ip),
user_agent.as_deref(),
)
.await
}
#[derive(Debug, Deserialize)]
pub struct DelegationTokenAuthSubmit {
pub request_uri: String,
pub delegated_did: String,
}
pub async fn delegation_auth_token(
State(state): State<AppState>,
headers: HeaderMap,
auth: Auth<Active>,
Json(form): Json<DelegationTokenAuthSubmit>,
) -> Response {
let controller_did = &auth.did;
let delegated_did = match parse_did(&form.delegated_did, "delegated") {
Ok(d) => d,
Err(resp) => return resp,
};
let request = match get_auth_request(&state, &form.request_uri).await {
Ok(r) => r,
Err(resp) => return resp,
};
let grant = match get_delegation_grant(&state, &delegated_did, controller_did).await {
Ok(g) => g,
Err(resp) => return resp,
};
if let Err(resp) =
bind_delegation_to_request(&state, &form.request_uri, &delegated_did, controller_did).await
{
return resp;
}
let ip = extract_client_ip(&headers, None);
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
finalize_delegation_auth(
&state,
&form.request_uri,
&delegated_did,
controller_did,
serde_json::json!({
"client_id": request.client_id,
"granted_scopes": grant.granted_scopes,
"auth_method": "token"
}),
Some(&ip),
user_agent.as_deref(),
)
.await
}
#[derive(Debug, Deserialize)]
pub struct CrossPdsCallbackParams {
pub code: String,
pub state: String,
pub iss: Option<String>,
}
pub async fn delegation_callback(
State(state): State<AppState>,
_rate_limit: OAuthRateLimited<LoginLimit>,
Query(params): Query<CrossPdsCallbackParams>,
) -> Response {
let auth_state = match state
.cross_pds_oauth
.retrieve_auth_state(&params.state)
.await
{
Ok(s) => s,
Err(e) => {
tracing::error!("Failed to retrieve cross-PDS auth state: {:?}", e);
return (
axum::http::StatusCode::BAD_REQUEST,
"Cross-PDS auth state expired or invalid",
)
.into_response();
}
};
if let Some(ref expected_issuer) = auth_state.expected_issuer {
match &params.iss {
Some(iss) if iss != expected_issuer => {
tracing::error!(
"Cross-PDS issuer mismatch: expected {}, got {}",
expected_issuer,
iss
);
return (
axum::http::StatusCode::FORBIDDEN,
"Authorization server issuer mismatch",
)
.into_response();
}
None => {
tracing::error!(
"Cross-PDS callback missing iss parameter (expected {}), possible mix-up attack",
expected_issuer
);
return (
axum::http::StatusCode::BAD_REQUEST,
"Missing required iss parameter",
)
.into_response();
}
_ => {}
}
}
let hostname = &tranquil_config::get().server.hostname;
let urls = delegation_oauth_urls(hostname);
let returned_sub = match state
.cross_pds_oauth
.exchange_code(
&auth_state,
&params.code,
&urls.client_id,
&urls.redirect_uri,
)
.await
{
Ok(sub) => sub,
Err(e) => {
tracing::error!("Cross-PDS token exchange failed: {:?}", e);
return (
axum::http::StatusCode::BAD_GATEWAY,
"Controller authentication failed",
)
.into_response();
}
};
if returned_sub != auth_state.controller_did.as_str() {
tracing::error!(
"Cross-PDS DID mismatch: expected {}, got {}",
auth_state.controller_did,
returned_sub
);
return (axum::http::StatusCode::FORBIDDEN, "Controller DID mismatch").into_response();
}
let delegated_did = &auth_state.delegated_did;
let controller_did = &auth_state.controller_did;
if let Err(_) = get_delegation_grant(&state, delegated_did, controller_did).await {
tracing::warn!(
"Delegation grant revoked during cross-PDS auth: {} -> {}",
controller_did,
delegated_did
);
return (
axum::http::StatusCode::FORBIDDEN,
"Delegation grant has been revoked",
)
.into_response();
}
if let Err(resp) = bind_delegation_to_request(
&state,
&auth_state.original_request_uri,
delegated_did,
controller_did,
)
.await
{
return resp;
}
let _ = state
.delegation_repo
.log_delegation_action(
delegated_did,
controller_did,
Some(controller_did),
DelegationActionType::TokenIssued,
Some(serde_json::json!({
"auth_method": "cross_pds",
"controller_pds": auth_state.controller_pds_url
})),
None,
None,
)
.await;
Redirect::temporary(&consent_url(&auth_state.original_request_uri)).into_response()
}
pub async fn delegation_client_metadata(State(_state): State<AppState>) -> Response {
let hostname = &tranquil_config::get().server.hostname;
let metadata = build_client_metadata(hostname);
Json(metadata).into_response()
}
@@ -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<AppState>) -> Json<JwkSet> {
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(),
@@ -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<ClientAuth, OAuthError>
fn validate_scope(
requested_scope: &Option<String>,
client_metadata: &crate::oauth::ClientMetadata,
client_metadata: &tranquil_pds::oauth::ClientMetadata,
) -> Result<Option<String>, OAuthError> {
let scope_str = match requested_scope {
Some(s) if !s.is_empty() => s,
@@ -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,
@@ -1,10 +1,10 @@
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;
use hmac::Mac;
use sha2::{Digest, Sha256};
use sha2::Sha256;
use subtle::ConstantTimeEq;
const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300;
@@ -17,10 +17,7 @@ pub struct TokenClaims {
}
pub fn verify_pkce(code_challenge: &str, code_verifier: &str) -> Result<(), OAuthError> {
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let hash = hasher.finalize();
let computed_challenge = URL_SAFE_NO_PAD.encode(hash);
let computed_challenge = tranquil_pds::oauth::compute_pkce_challenge(code_verifier);
if !bool::from(
computed_challenge
.as_bytes()
@@ -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)
@@ -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()?;
@@ -1,4 +1,4 @@
use crate::oauth::OAuthError;
use tranquil_pds::oauth::OAuthError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
+124
View File
@@ -0,0 +1,124 @@
pub mod endpoints;
pub mod jwks;
pub mod sso_endpoints;
use tranquil_pds::state::AppState;
pub fn oauth_routes() -> axum::Router<AppState> {
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(
"/delegation/callback",
get(endpoints::delegation_callback),
)
.route(
"/delegation/client-metadata",
get(endpoints::delegation_client_metadata),
)
.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<AppState> {
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<AppState> {
use axum::routing::get;
axum::Router::new()
.route(
"/oauth-client-metadata.json",
get(endpoints::frontend_client_metadata),
)
}
@@ -9,22 +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;
fn generate_state() -> String {
use rand::RngCore;
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
use tranquil_pds::state::AppState;
fn generate_nonce() -> String {
use rand::RngCore;
@@ -129,7 +122,7 @@ pub async fn sso_initiate(
}
};
let sso_state = generate_state();
let sso_state = tranquil_pds::util::generate_random_token();
let nonce = generate_nonce();
let redirect_uri = SsoConfig::get_redirect_uri();
@@ -367,7 +360,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 +475,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 +548,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 +609,7 @@ pub struct LinkedAccountsResponse {
pub async fn get_linked_accounts(
State(state): State<AppState>,
auth: crate::auth::Auth<crate::auth::Active>,
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
) -> Result<Json<LinkedAccountsResponse>, ApiError> {
let identities = state
.sso_repo
@@ -651,7 +644,7 @@ pub struct UnlinkAccountResponse {
pub async fn unlink_account(
State(state): State<AppState>,
auth: crate::auth::Auth<crate::auth::Active>,
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
Json(input): Json<UnlinkAccountRequest>,
) -> Result<Json<UnlinkAccountResponse>, ApiError> {
let _rate_limit = check_user_rate_limit_with_message::<SsoUnlinkLimit>(
@@ -763,7 +756,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 {
@@ -774,14 +767,14 @@ pub async fn check_handle_available(
};
let available_domains = tranquil_config::get().server.available_user_domain_list();
if let Some(ref d) = query.domain {
if !available_domains.iter().any(|ad| ad == d) {
return Err(ApiError::InvalidRequest("Unknown user domain".into()));
}
if let Some(ref d) = query.domain
&& !available_domains.iter().any(|ad| ad == d)
{
return Err(ApiError::InvalidRequest("Unknown user domain".into()));
}
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 +872,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 +907,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 +919,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 +953,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 +974,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 +1001,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 +1031,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 +1048,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 +1064,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 +1082,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 +1139,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 +1182,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 +1201,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 +1280,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 +1291,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 +1300,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);
@@ -1323,7 +1316,7 @@ pub async fn complete_registration(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -1333,7 +1326,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 +1363,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,
+81
View File
@@ -385,6 +385,87 @@ pub fn compute_access_token_hash(access_token: &str) -> String {
URL_SAFE_NO_PAD.encode(hash)
}
pub fn compute_pkce_challenge(verifier: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
URL_SAFE_NO_PAD.encode(hasher.finalize())
}
pub fn es256_signing_key_to_jwk(key: &p256::ecdsa::SigningKey) -> Result<DPoPJwk, OAuthError> {
let point = key.verifying_key().to_encoded_point(false);
let x = URL_SAFE_NO_PAD.encode(
point
.x()
.ok_or_else(|| OAuthError::InvalidDpopProof("invalid EC key: missing x".into()))?,
);
let y = URL_SAFE_NO_PAD.encode(
point
.y()
.ok_or_else(|| OAuthError::InvalidDpopProof("invalid EC key: missing y".into()))?,
);
Ok(DPoPJwk {
kty: "EC".to_string(),
crv: Some("P-256".to_string()),
x: Some(x),
y: Some(y),
})
}
pub fn create_dpop_proof(
signing_key: &p256::ecdsa::SigningKey,
method: &str,
url: &str,
nonce: Option<&str>,
access_token_hash: Option<&str>,
) -> Result<String, OAuthError> {
use p256::ecdsa::signature::Signer;
let jwk = es256_signing_key_to_jwk(signing_key)?;
let header = serde_json::json!({
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": jwk
});
let jti = {
use rand::Rng;
let bytes: [u8; 16] = rand::thread_rng().r#gen();
URL_SAFE_NO_PAD.encode(bytes)
};
let mut payload = serde_json::json!({
"jti": jti,
"htm": method,
"htu": url,
"iat": Utc::now().timestamp()
});
if let Some(n) = nonce {
payload["nonce"] = serde_json::Value::String(n.to_string());
}
if let Some(ath) = access_token_hash {
payload["ath"] = serde_json::Value::String(ath.to_string());
}
let header_b64 = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&header).map_err(|e| OAuthError::InvalidDpopProof(e.to_string()))?,
);
let payload_b64 = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&payload).map_err(|e| OAuthError::InvalidDpopProof(e.to_string()))?,
);
let signing_input = format!("{}.{}", header_b64, payload_b64);
let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
}
pub fn compute_es256_jkt(signing_key: &p256::ecdsa::SigningKey) -> Result<String, OAuthError> {
let jwk = es256_signing_key_to_jwk(signing_key)?;
compute_jwk_thumbprint(&jwk)
}
#[cfg(test)]
mod tests {
use super::*;
+3 -1
View File
@@ -6,7 +6,9 @@ mod types;
pub use client::{ClientMetadata, ClientMetadataCache, verify_client_auth};
pub use dpop::{
DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult,
compute_access_token_hash, compute_jwk_thumbprint,
compute_access_token_hash, compute_es256_jkt, compute_jwk_thumbprint, compute_pkce_challenge,
create_dpop_proof,
es256_signing_key_to_jwk,
};
pub use error::OAuthError;
pub use types::{
+3 -3
View File
@@ -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 }
-14
View File
@@ -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};
+25
View File
@@ -308,6 +308,31 @@ pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationErr
validate_service_handle(handle, ReservedHandlePolicy::Reject)
}
pub fn resolve_handle_input(input: &str) -> Result<String, HandleValidationError> {
let available_domains = tranquil_config::get().server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
if !input.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.strip_suffix(&format!(".{}", domain))
.unwrap_or(input),
None => input,
};
let validated = validate_short_handle(handle_to_validate)?;
Ok(format!(
"{}.{}",
validated,
matched_domain.unwrap_or(&available_domains[0])
))
} else {
validate_full_domain_handle(input)
}
}
pub fn validate_service_handle(
handle: &str,
reserved_policy: ReservedHandlePolicy,
+3 -3
View File
@@ -74,7 +74,7 @@ pub async fn require_legacy_session_mfa<'a>(
state: &AppState,
user: &'a AuthenticatedUser,
) -> Result<MfaVerified<'a>, 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<MfaVerified<'a>, 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<Option<MfaVerified<'a>>, 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
+5 -3
View File
@@ -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;
@@ -205,7 +206,7 @@ impl AuthenticatedUser {
return ScopePermissions::from_scope_string(Some(scope));
}
if !self.is_oauth() {
return ScopePermissions::from_scope_string(Some("atproto"));
return ScopePermissions::from_scope_string(Some("transition:generic transition:chat.bsky"));
}
ScopePermissions::from_scope_string(self.scope.as_deref())
}
@@ -355,7 +356,7 @@ async fn validate_bearer_token_with_options_internal(
)
.await;
let status_cache_key = crate::cache_keys::user_status_key(&did.to_string());
let status_cache_key = crate::cache_keys::user_status_key(did.as_ref());
let cached = CachedUserStatus {
deactivated: user.deactivated_at.is_some(),
takendown: user.takedown_ref.is_some(),
@@ -394,7 +395,7 @@ async fn validate_bearer_token_with_options_internal(
match verify_access_token_typed(token, &decrypted_key) {
Ok(token_data) => {
let jti = &token_data.claims.jti;
let session_cache_key = crate::cache_keys::session_key(&did, &jti);
let session_cache_key = crate::cache_keys::session_key(&did, jti);
let mut session_valid = false;
if let Some(c) = cache {
@@ -530,6 +531,7 @@ pub enum AccountRequirement {
AnyStatus,
}
#[allow(clippy::too_many_arguments)]
pub async fn validate_token_with_dpop(
user_repo: &dyn UserRepository,
oauth_repo: &dyn OAuthRepository,
+156
View File
@@ -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<chrono::DateTime<Utc>>) -> 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<ReauthMethod> {
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<dyn crate::cache::Cache>,
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::<i64>()
{
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<ReauthMethod>,
}
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<ReauthMethod>,
}
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()
}
+1 -1
View File
@@ -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};
+42 -3
View File
@@ -2,11 +2,50 @@ pub mod roles;
pub mod scopes;
pub use roles::{
CanAddControllers, CanBeController, CanControlAccounts, verify_can_add_controllers,
verify_can_be_controller, verify_can_control_accounts,
CanAddControllers, CanControlAccounts, verify_can_add_controllers,
verify_can_control_accounts,
};
pub use scopes::{
InvalidDelegationScopeError, SCOPE_PRESETS, ScopePreset, ValidatedDelegationScope,
intersect_scopes, validate_delegation_scopes,
intersect_scopes,
};
pub use tranquil_db_traits::DelegationActionType;
use crate::state::AppState;
use crate::types::Did;
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedIdentity {
pub did: Did,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pds_url: Option<String>,
pub is_local: bool,
}
pub async fn resolve_identity(state: &AppState, did: &Did) -> Option<ResolvedIdentity> {
let is_local = state
.user_repo
.get_by_did(did)
.await
.ok()
.flatten()
.is_some();
let did_doc = state
.did_resolver
.resolve_did_document(did.as_str())
.await?;
let pds_url = tranquil_types::did_doc::extract_pds_endpoint(&did_doc);
let handle = tranquil_types::did_doc::extract_handle(&did_doc);
Some(ResolvedIdentity {
did: did.clone(),
handle,
pds_url,
is_local,
})
}

Some files were not shown because too many files have changed in this diff Show More