mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-16 14:26:06 +00:00
refactor: toml config
This commit is contained in:
+2
-2
@@ -131,13 +131,13 @@ BACKUP_STORAGE_PATH=/var/lib/tranquil/backups
|
||||
# Account Registration
|
||||
# =============================================================================
|
||||
# Require invite codes for registration
|
||||
# INVITE_CODE_REQUIRED=false
|
||||
# INVITE_CODE_REQUIRED=true
|
||||
# Comma-separated list of available user domains
|
||||
# AVAILABLE_USER_DOMAINS=example.com
|
||||
# Enable self-hosted did:web identities (default: true)
|
||||
# Hosting did:web requires a long-term commitment to serve DID documents.
|
||||
# Set to false if you don't want to offer this option.
|
||||
# ENABLE_SELF_HOSTED_DID_WEB=true
|
||||
# ENABLE_PDS_HOSTED_DID_WEB=false
|
||||
# =============================================================================
|
||||
# Server Metadata (returned by describeServer)
|
||||
# =============================================================================
|
||||
|
||||
Generated
+210
-3
@@ -104,6 +104,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
@@ -1216,6 +1266,46 @@ dependencies = [
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
@@ -1240,6 +1330,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1280,6 +1376,29 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "confique"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06b4f5ec222421e22bb0a8cbaa36b1d2b50fd45cdd30c915ded34108da78b29f"
|
||||
dependencies = [
|
||||
"confique-macro",
|
||||
"serde",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "confique-macro"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4d1754680cd218e7bcb4c960cc9bae3444b5197d64563dccccfdf83cab9e1a7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@@ -2762,7 +2881,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -3059,6 +3178,12 @@ dependencies = [
|
||||
"unsigned-varint 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -3715,6 +3840,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
@@ -4209,7 +4340,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.35",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.2",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4246,7 +4377,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@@ -4909,6 +5040,15 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -5708,6 +5848,45 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap 2.12.1",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.14.2"
|
||||
@@ -5908,6 +6087,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"subtle",
|
||||
"totp-rs",
|
||||
"tranquil-config",
|
||||
"tranquil-crypto",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
@@ -5922,6 +6102,7 @@ dependencies = [
|
||||
"redis",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tranquil-config",
|
||||
"tranquil-infra",
|
||||
"tranquil-ripple",
|
||||
]
|
||||
@@ -5936,10 +6117,19 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tranquil-config",
|
||||
"tranquil-db-traits",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-config"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"confique",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-crypto"
|
||||
version = "0.2.1"
|
||||
@@ -5997,6 +6187,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"thiserror 2.0.17",
|
||||
"tranquil-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6041,6 +6232,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"cid",
|
||||
"clap",
|
||||
"ctor",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
@@ -6091,6 +6283,7 @@ dependencies = [
|
||||
"tranquil-auth",
|
||||
"tranquil-cache",
|
||||
"tranquil-comms",
|
||||
"tranquil-config",
|
||||
"tranquil-crypto",
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
@@ -6139,6 +6332,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tranquil-config",
|
||||
"tranquil-infra",
|
||||
"uuid",
|
||||
]
|
||||
@@ -6171,6 +6365,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tranquil-config",
|
||||
"tranquil-infra",
|
||||
"uuid",
|
||||
]
|
||||
@@ -6356,6 +6551,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
@@ -6930,6 +7131,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/tranquil-types",
|
||||
"crates/tranquil-config",
|
||||
"crates/tranquil-infra",
|
||||
"crates/tranquil-crypto",
|
||||
"crates/tranquil-storage",
|
||||
@@ -24,6 +25,7 @@ license = "AGPL-3.0-or-later"
|
||||
|
||||
[workspace.dependencies]
|
||||
tranquil-types = { path = "crates/tranquil-types" }
|
||||
tranquil-config = { path = "crates/tranquil-config" }
|
||||
tranquil-infra = { path = "crates/tranquil-infra" }
|
||||
tranquil-crypto = { path = "crates/tranquil-crypto" }
|
||||
tranquil-storage = { path = "crates/tranquil-storage" }
|
||||
@@ -52,6 +54,8 @@ bs58 = "0.5"
|
||||
bytes = "1.11"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
cid = "0.11"
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
confique = { version = "0.4", features = ["toml"] }
|
||||
dotenvy = "0.15"
|
||||
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
|
||||
foca = { version = "1", features = ["bincode-codec", "tracing"] }
|
||||
|
||||
@@ -24,7 +24,10 @@ just run
|
||||
|
||||
## Configuration
|
||||
|
||||
See `.env.example` for all configuration options.
|
||||
See `example.toml` for all configuration options.
|
||||
|
||||
> [!NOTE]
|
||||
> The order of configuration precendence is: environment variables, than a config file passed via `--config`, than `/etc/tranquil-pds/config.toml`, than the built-in defaults. So you can use environment variables, or a config file, or both.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-crypto = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -127,7 +127,9 @@ fn create_signed_token_with_act(
|
||||
let jti = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| {
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
|
||||
tranquil_config::try_get()
|
||||
.map(|c| c.server.hostname.clone())
|
||||
.unwrap_or_else(|| "localhost".to_string())
|
||||
});
|
||||
|
||||
let claims = Claims {
|
||||
@@ -253,7 +255,9 @@ fn create_hs256_token_with_metadata(
|
||||
sub: did.to_owned(),
|
||||
aud: format!(
|
||||
"did:web:{}",
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
|
||||
tranquil_config::try_get()
|
||||
.map(|c| c.server.hostname.clone())
|
||||
.unwrap_or_else(|| "localhost".to_string())
|
||||
),
|
||||
exp: expiration,
|
||||
iat: Utc::now().timestamp(),
|
||||
|
||||
@@ -9,6 +9,7 @@ default = []
|
||||
valkey = ["dep:redis"]
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-infra = { workspace = true }
|
||||
tranquil-ripple = { workspace = true }
|
||||
|
||||
|
||||
@@ -172,28 +172,36 @@ impl DistributedRateLimiter for NoOpRateLimiter {
|
||||
pub async fn create_cache(
|
||||
shutdown: tokio_util::sync::CancellationToken,
|
||||
) -> (Arc<dyn Cache>, Arc<dyn DistributedRateLimiter>) {
|
||||
let cache_cfg = tranquil_config::try_get().map(|c| &c.cache);
|
||||
let backend = cache_cfg.map(|c| c.backend.as_str()).unwrap_or("ripple");
|
||||
let valkey_url = cache_cfg.and_then(|c| c.valkey_url.as_deref());
|
||||
|
||||
#[cfg(feature = "valkey")]
|
||||
if let Ok(url) = std::env::var("VALKEY_URL") {
|
||||
match ValkeyCache::new(&url).await {
|
||||
Ok(cache) => {
|
||||
tracing::info!("using valkey cache at {url}");
|
||||
let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection()));
|
||||
return (Arc::new(cache), rate_limiter);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to connect to valkey: {e}. falling back to ripple.");
|
||||
if backend == "valkey" {
|
||||
if let Some(url) = valkey_url {
|
||||
match ValkeyCache::new(url).await {
|
||||
Ok(cache) => {
|
||||
tracing::info!("using valkey cache at {url}");
|
||||
let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection()));
|
||||
return (Arc::new(cache), rate_limiter);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to connect to valkey: {e}. falling back to ripple.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("cache.backend is \"valkey\" but VALKEY_URL is not set. using ripple.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "valkey"))]
|
||||
if std::env::var("VALKEY_URL").is_ok() {
|
||||
if backend == "valkey" {
|
||||
tracing::warn!(
|
||||
"VALKEY_URL is set but binary was compiled without valkey feature. using ripple."
|
||||
"cache.backend is \"valkey\" but binary was compiled without valkey feature. using ripple."
|
||||
);
|
||||
}
|
||||
|
||||
match tranquil_ripple::RippleConfig::from_env() {
|
||||
match tranquil_ripple::RippleConfig::from_config() {
|
||||
Ok(config) => {
|
||||
let peer_count = config.seed_peers.len();
|
||||
match tranquil_ripple::RippleEngine::start(config, shutdown).await {
|
||||
@@ -205,13 +213,13 @@ pub async fn create_cache(
|
||||
(cache, rate_limiter)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("ripple engine failed to start: {e}. running without cache.");
|
||||
tracing::error!("ripple engine failed to start: {e:#}. running without cache.");
|
||||
(Arc::new(NoOpCache), Arc::new(NoOpRateLimiter))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("ripple config error: {e}. running without cache.");
|
||||
tracing::error!("ripple config error: {e:#}. running without cache.");
|
||||
(Arc::new(NoOpCache), Arc::new(NoOpRateLimiter))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
|
||||
@@ -112,20 +112,19 @@ pub struct EmailSender {
|
||||
}
|
||||
|
||||
impl EmailSender {
|
||||
pub fn new(from_address: String, from_name: String) -> Self {
|
||||
pub fn new(from_address: String, from_name: String, sendmail_path: String) -> Self {
|
||||
Self {
|
||||
from_address,
|
||||
from_name,
|
||||
sendmail_path: std::env::var("SENDMAIL_PATH")
|
||||
.unwrap_or_else(|_| "/usr/sbin/sendmail".to_string()),
|
||||
sendmail_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let from_address = std::env::var("MAIL_FROM_ADDRESS").ok()?;
|
||||
let from_name =
|
||||
std::env::var("MAIL_FROM_NAME").unwrap_or_else(|_| "Tranquil PDS".to_string());
|
||||
Some(Self::new(from_address, from_name))
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let from_address = cfg.email.from_address.clone()?;
|
||||
let from_name = cfg.email.from_name.clone();
|
||||
let sendmail_path = cfg.email.sendmail_path.clone();
|
||||
Some(Self::new(from_address, from_name, sendmail_path))
|
||||
}
|
||||
|
||||
pub fn format_email(&self, notification: &QueuedComms) -> String {
|
||||
@@ -190,8 +189,8 @@ impl DiscordSender {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let bot_token = std::env::var("DISCORD_BOT_TOKEN").ok()?;
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let bot_token = cfg.discord.bot_token.clone()?;
|
||||
Some(Self::new(bot_token))
|
||||
}
|
||||
|
||||
@@ -454,8 +453,8 @@ impl TelegramSender {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let bot_token = std::env::var("TELEGRAM_BOT_TOKEN").ok()?;
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let bot_token = cfg.telegram.bot_token.clone()?;
|
||||
Some(Self::new(bot_token))
|
||||
}
|
||||
|
||||
@@ -586,10 +585,9 @@ impl SignalSender {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let signal_cli_path = std::env::var("SIGNAL_CLI_PATH")
|
||||
.unwrap_or_else(|_| "/usr/local/bin/signal-cli".to_string());
|
||||
let sender_number = std::env::var("SIGNAL_SENDER_NUMBER").ok()?;
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let signal_cli_path = cfg.signal.cli_path.clone();
|
||||
let sender_number = cfg.signal.sender_number.clone()?;
|
||||
Some(Self::new(signal_cli_path, sender_number))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "tranquil-config"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
confique = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -0,0 +1,883 @@
|
||||
use confique::Config;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static CONFIG: OnceLock<TranquilConfig> = OnceLock::new();
|
||||
|
||||
/// Errors discovered during configuration validation.
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigError {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "configuration validation failed:")?;
|
||||
for err in &self.errors {
|
||||
writeln!(f, " - {err}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
/// Initialize the global configuration. Must be called once at startup before
|
||||
/// any other code accesses the configuration. Panics if called more than once.
|
||||
pub fn init(config: TranquilConfig) {
|
||||
CONFIG
|
||||
.set(config)
|
||||
.expect("tranquil-config: configuration already initialized");
|
||||
}
|
||||
|
||||
/// Returns a reference to the global configuration.
|
||||
/// Panics if [`init`] has not been called yet.
|
||||
pub fn get() -> &'static TranquilConfig {
|
||||
CONFIG
|
||||
.get()
|
||||
.expect("tranquil-config: not initialized - call tranquil_config::init() first")
|
||||
}
|
||||
|
||||
/// Returns a reference to the global configuration if it has been initialized.
|
||||
pub fn try_get() -> Option<&'static TranquilConfig> {
|
||||
CONFIG.get()
|
||||
}
|
||||
|
||||
/// Load configuration from an optional TOML file path, with environment
|
||||
/// variable overrides applied on top. Fields annotated with `#[config(env)]`
|
||||
/// are read from the corresponding environment variables when the `.env()`
|
||||
/// layer is active.
|
||||
///
|
||||
/// Precedence (highest to lowest):
|
||||
/// 1. Environment variables
|
||||
/// 2. TOML config file (if provided)
|
||||
/// 3. Built-in defaults
|
||||
pub fn load(config_path: Option<&PathBuf>) -> Result<TranquilConfig, confique::Error> {
|
||||
let mut builder = TranquilConfig::builder().env();
|
||||
if let Some(path) = config_path {
|
||||
builder = builder.file(path);
|
||||
}
|
||||
builder.file("/etc/tranquil-pds/config.toml").load()
|
||||
}
|
||||
|
||||
// Root configuration
|
||||
#[derive(Debug, Config)]
|
||||
pub struct TranquilConfig {
|
||||
#[config(nested)]
|
||||
pub server: ServerConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub database: DatabaseConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub secrets: SecretsConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub storage: StorageConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub backup: BackupConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub cache: CacheConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub plc: PlcConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub firehose: FirehoseConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub email: EmailConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub discord: DiscordConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub telegram: TelegramConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub signal: SignalConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub notifications: NotificationConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub sso: SsoConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub moderation: ModerationConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub import: ImportConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub scheduled: ScheduledConfig,
|
||||
}
|
||||
|
||||
impl TranquilConfig {
|
||||
/// Validate cross-field constraints that cannot be expressed through
|
||||
/// confique's declarative defaults alone. Call this once after loading
|
||||
/// the configuration and before [`init`].
|
||||
///
|
||||
/// Returns `Ok(())` when the configuration is consistent, or a
|
||||
/// [`ConfigError`] listing every problem found.
|
||||
pub fn validate(&self, ignore_secrets: bool) -> Result<(), ConfigError> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// -- secrets ----------------------------------------------------------
|
||||
if !ignore_secrets && !self.secrets.allow_insecure && !cfg!(test) {
|
||||
if let Some(ref s) = self.secrets.jwt_secret {
|
||||
if s.len() < 32 {
|
||||
errors.push(
|
||||
"secrets.jwt_secret (JWT_SECRET) must be at least 32 characters"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
errors.push(
|
||||
"secrets.jwt_secret (JWT_SECRET) is required in production \
|
||||
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref s) = self.secrets.dpop_secret {
|
||||
if s.len() < 32 {
|
||||
errors.push(
|
||||
"secrets.dpop_secret (DPOP_SECRET) must be at least 32 characters"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
errors.push(
|
||||
"secrets.dpop_secret (DPOP_SECRET) is required in production \
|
||||
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref s) = self.secrets.master_key {
|
||||
if s.len() < 32 {
|
||||
errors.push(
|
||||
"secrets.master_key (MASTER_KEY) must be at least 32 characters"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
errors.push(
|
||||
"secrets.master_key (MASTER_KEY) is required in production \
|
||||
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- telegram ---------------------------------------------------------
|
||||
if self.telegram.bot_token.is_some() && self.telegram.webhook_secret.is_none() {
|
||||
errors.push(
|
||||
"telegram.bot_token is set but telegram.webhook_secret is missing; \
|
||||
both are required for secure Telegram integration"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// -- blob storage -----------------------------------------------------
|
||||
match self.storage.backend.as_str() {
|
||||
"s3" => {
|
||||
if self.storage.s3_bucket.is_none() {
|
||||
errors.push(
|
||||
"storage.backend is \"s3\" but storage.s3_bucket (S3_BUCKET) \
|
||||
is not set"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"filesystem" => {}
|
||||
other => {
|
||||
errors.push(format!(
|
||||
"storage.backend must be \"filesystem\" or \"s3\", got \"{other}\""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// -- backup storage ---------------------------------------------------
|
||||
if self.backup.enabled {
|
||||
match self.backup.backend.as_str() {
|
||||
"s3" => {
|
||||
if self.backup.s3_bucket.is_none() {
|
||||
errors.push(
|
||||
"backup.backend is \"s3\" but backup.s3_bucket \
|
||||
(BACKUP_S3_BUCKET) is not set"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"filesystem" => {}
|
||||
other => {
|
||||
errors.push(format!(
|
||||
"backup.backend must be \"filesystem\" or \"s3\", got \"{other}\""
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- SSO providers ----------------------------------------------------
|
||||
self.validate_sso_provider("sso.github", &self.sso.github, &mut errors);
|
||||
self.validate_sso_provider("sso.google", &self.sso.google, &mut errors);
|
||||
self.validate_sso_discord(&mut errors);
|
||||
self.validate_sso_with_issuer("sso.gitlab", &self.sso.gitlab, &mut errors);
|
||||
self.validate_sso_with_issuer("sso.oidc", &self.sso.oidc, &mut errors);
|
||||
self.validate_sso_apple(&mut errors);
|
||||
|
||||
// -- moderation -------------------------------------------------------
|
||||
let has_url = self.moderation.report_service_url.is_some();
|
||||
let has_did = self.moderation.report_service_did.is_some();
|
||||
if has_url != has_did {
|
||||
errors.push(
|
||||
"moderation.report_service_url and moderation.report_service_did \
|
||||
must both be set or both be unset"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// -- cache ------------------------------------------------------------
|
||||
match self.cache.backend.as_str() {
|
||||
"valkey" => {
|
||||
if self.cache.valkey_url.is_none() {
|
||||
errors.push(
|
||||
"cache.backend is \"valkey\" but cache.valkey_url (VALKEY_URL) \
|
||||
is not set"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"ripple" => {}
|
||||
other => {
|
||||
errors.push(format!(
|
||||
"cache.backend must be \"ripple\" or \"valkey\", got \"{other}\""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConfigError { errors })
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_sso_provider(&self, prefix: &str, p: &SsoProviderConfig, errors: &mut Vec<String>) {
|
||||
if p.enabled {
|
||||
if p.client_id.is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.client_id is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
if p.client_secret.is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.client_secret is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_sso_discord(&self, errors: &mut Vec<String>) {
|
||||
let p = &self.sso.discord;
|
||||
if p.enabled {
|
||||
if p.client_id.is_none() {
|
||||
errors.push(
|
||||
"sso.discord.client_id is required when sso.discord.enabled = true".to_string(),
|
||||
);
|
||||
}
|
||||
if p.client_secret.is_none() {
|
||||
errors.push(
|
||||
"sso.discord.client_secret is required when sso.discord.enabled = true"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_sso_with_issuer(
|
||||
&self,
|
||||
prefix: &str,
|
||||
p: &SsoProviderWithIssuerConfig,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
if p.enabled {
|
||||
if p.client_id.is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.client_id is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
if p.client_secret.is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.client_secret is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
if p.issuer.is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.issuer is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_sso_apple(&self, errors: &mut Vec<String>) {
|
||||
let p = &self.sso.apple;
|
||||
if p.enabled {
|
||||
if p.client_id.is_none() {
|
||||
errors.push(
|
||||
"sso.apple.client_id is required when sso.apple.enabled = true".to_string(),
|
||||
);
|
||||
}
|
||||
if p.team_id.is_none() {
|
||||
errors.push(
|
||||
"sso.apple.team_id is required when sso.apple.enabled = true".to_string(),
|
||||
);
|
||||
}
|
||||
if p.key_id.is_none() {
|
||||
errors
|
||||
.push("sso.apple.key_id is required when sso.apple.enabled = true".to_string());
|
||||
}
|
||||
if p.private_key.is_none() {
|
||||
errors.push(
|
||||
"sso.apple.private_key is required when sso.apple.enabled = true".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct ServerConfig {
|
||||
/// Public hostname of the PDS (e.g. `pds.example.com`).
|
||||
#[config(env = "PDS_HOSTNAME")]
|
||||
pub hostname: String,
|
||||
|
||||
/// Address to bind the HTTP server to.
|
||||
#[config(env = "SERVER_HOST", default = "127.0.0.1")]
|
||||
pub host: String,
|
||||
|
||||
/// Port to bind the HTTP server to.
|
||||
#[config(env = "SERVER_PORT", default = 3000)]
|
||||
pub port: u16,
|
||||
|
||||
/// List of domains for user handles.
|
||||
/// Defaults to the PDS hostname when not set.
|
||||
#[config(env = "PDS_USER_HANDLE_DOMAINS", parse_env = split_comma_list)]
|
||||
pub user_handle_domains: Option<Vec<String>>,
|
||||
|
||||
/// List of domains available for user registration.
|
||||
/// Defaults to the PDS hostname when not set.
|
||||
#[config(env = "AVAILABLE_USER_DOMAINS", parse_env = split_comma_list)]
|
||||
pub available_user_domains: Option<Vec<String>>,
|
||||
|
||||
/// Enable PDS-hosted did:web identities. Hosting did:web requires a
|
||||
/// long-term commitment to serve DID documents; opt-in only.
|
||||
#[config(env = "ENABLE_PDS_HOSTED_DID_WEB", default = false)]
|
||||
pub enable_pds_hosted_did_web: bool,
|
||||
|
||||
/// When set to true, skip age-assurance birthday prompt for all accounts.
|
||||
#[config(env = "PDS_AGE_ASSURANCE_OVERRIDE", default = false)]
|
||||
pub age_assurance_override: bool,
|
||||
|
||||
/// Require an invite code for new account registration.
|
||||
#[config(env = "INVITE_CODE_REQUIRED", default = true)]
|
||||
pub invite_code_required: bool,
|
||||
|
||||
/// Allow HTTP (non-TLS) proxy requests. Only useful during development.
|
||||
#[config(env = "ALLOW_HTTP_PROXY", default = false)]
|
||||
pub allow_http_proxy: bool,
|
||||
|
||||
/// Disable all rate limiting. Should only be used in testing.
|
||||
#[config(env = "DISABLE_RATE_LIMITING", default = false)]
|
||||
pub disable_rate_limiting: bool,
|
||||
|
||||
/// List of additional banned words for handle validation.
|
||||
#[config(env = "PDS_BANNED_WORDS", parse_env = split_comma_list)]
|
||||
pub banned_words: Option<Vec<String>>,
|
||||
|
||||
/// URL to a privacy policy page.
|
||||
#[config(env = "PRIVACY_POLICY_URL")]
|
||||
pub privacy_policy_url: Option<String>,
|
||||
|
||||
/// URL to terms of service page.
|
||||
#[config(env = "TERMS_OF_SERVICE_URL")]
|
||||
pub terms_of_service_url: Option<String>,
|
||||
|
||||
/// Operator contact email address.
|
||||
#[config(env = "CONTACT_EMAIL")]
|
||||
pub contact_email: Option<String>,
|
||||
|
||||
/// Maximum allowed blob size in bytes (default 10 GiB).
|
||||
#[config(env = "MAX_BLOB_SIZE", default = 10_737_418_240u64)]
|
||||
pub max_blob_size: u64,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
/// The public HTTPS URL for this PDS.
|
||||
pub fn public_url(&self) -> String {
|
||||
format!("https://{}", self.hostname)
|
||||
}
|
||||
|
||||
/// Hostname without port suffix (e.g. `pds.example.com` from
|
||||
/// `pds.example.com:443`).
|
||||
pub fn hostname_without_port(&self) -> &str {
|
||||
self.hostname.split(':').next().unwrap_or(&self.hostname)
|
||||
}
|
||||
|
||||
/// Returns the extra banned words list, or an empty vec when unset.
|
||||
pub fn banned_word_list(&self) -> Vec<String> {
|
||||
self.banned_words.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the available user domains, falling back to `[hostname_without_port]`.
|
||||
pub fn available_user_domain_list(&self) -> Vec<String> {
|
||||
self.available_user_domains
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![self.hostname_without_port().to_string()])
|
||||
}
|
||||
|
||||
/// Returns the user handle domains, falling back to `[hostname_without_port]`.
|
||||
pub fn user_handle_domain_list(&self) -> Vec<String> {
|
||||
self.user_handle_domains
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![self.hostname_without_port().to_string()])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct DatabaseConfig {
|
||||
/// PostgreSQL connection URL.
|
||||
#[config(env = "DATABASE_URL")]
|
||||
pub url: String,
|
||||
|
||||
/// Maximum number of connections in the pool.
|
||||
#[config(env = "DATABASE_MAX_CONNECTIONS", default = 100)]
|
||||
pub max_connections: u32,
|
||||
|
||||
/// Minimum number of idle connections kept in the pool.
|
||||
#[config(env = "DATABASE_MIN_CONNECTIONS", default = 10)]
|
||||
pub min_connections: u32,
|
||||
|
||||
/// Timeout in seconds when acquiring a connection from the pool.
|
||||
#[config(env = "DATABASE_ACQUIRE_TIMEOUT_SECS", default = 10)]
|
||||
pub acquire_timeout_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
pub struct SecretsConfig {
|
||||
/// Secret used for signing JWTs. Must be at least 32 characters in
|
||||
/// production.
|
||||
#[config(env = "JWT_SECRET")]
|
||||
pub jwt_secret: Option<String>,
|
||||
|
||||
/// Secret used for DPoP proof validation. Must be at least 32 characters
|
||||
/// in production.
|
||||
#[config(env = "DPOP_SECRET")]
|
||||
pub dpop_secret: Option<String>,
|
||||
|
||||
/// Master key used for key-encryption and HKDF derivation. Must be at
|
||||
/// least 32 characters in production.
|
||||
#[config(env = "MASTER_KEY")]
|
||||
pub master_key: Option<String>,
|
||||
|
||||
/// PLC rotation key (DID key). If not set, user-level keys are used.
|
||||
#[config(env = "PLC_ROTATION_KEY")]
|
||||
pub plc_rotation_key: Option<String>,
|
||||
|
||||
/// Allow insecure/test secrets. NEVER enable in production.
|
||||
#[config(env = "TRANQUIL_PDS_ALLOW_INSECURE_SECRETS", default = false)]
|
||||
pub allow_insecure: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecretsConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecretsConfig")
|
||||
.field(
|
||||
"jwt_secret",
|
||||
&self.jwt_secret.as_ref().map(|_| "[REDACTED]"),
|
||||
)
|
||||
.field(
|
||||
"dpop_secret",
|
||||
&self.dpop_secret.as_ref().map(|_| "[REDACTED]"),
|
||||
)
|
||||
.field(
|
||||
"master_key",
|
||||
&self.master_key.as_ref().map(|_| "[REDACTED]"),
|
||||
)
|
||||
.field(
|
||||
"plc_rotation_key",
|
||||
&self.plc_rotation_key.as_ref().map(|_| "[REDACTED]"),
|
||||
)
|
||||
.field("allow_insecure", &self.allow_insecure)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretsConfig {
|
||||
/// Resolve the JWT secret, falling back to an insecure default if
|
||||
/// `allow_insecure` is true.
|
||||
pub fn jwt_secret_or_default(&self) -> String {
|
||||
self.jwt_secret.clone().unwrap_or_else(|| {
|
||||
if cfg!(test) || self.allow_insecure {
|
||||
"test-jwt-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"JWT_SECRET must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the DPoP secret, falling back to an insecure default if
|
||||
/// `allow_insecure` is true.
|
||||
pub fn dpop_secret_or_default(&self) -> String {
|
||||
self.dpop_secret.clone().unwrap_or_else(|| {
|
||||
if cfg!(test) || self.allow_insecure {
|
||||
"test-dpop-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"DPOP_SECRET must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the master key, falling back to an insecure default if
|
||||
/// `allow_insecure` is true.
|
||||
pub fn master_key_or_default(&self) -> String {
|
||||
self.master_key.clone().unwrap_or_else(|| {
|
||||
if cfg!(test) || self.allow_insecure {
|
||||
"test-master-key-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"MASTER_KEY must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct StorageConfig {
|
||||
/// Storage backend: `filesystem` or `s3`.
|
||||
#[config(env = "BLOB_STORAGE_BACKEND", default = "filesystem")]
|
||||
pub backend: String,
|
||||
|
||||
/// Path on disk for the filesystem blob backend.
|
||||
#[config(env = "BLOB_STORAGE_PATH", default = "/var/lib/tranquil-pds/blobs")]
|
||||
pub path: String,
|
||||
|
||||
/// S3 bucket name for blob storage.
|
||||
#[config(env = "S3_BUCKET")]
|
||||
pub s3_bucket: Option<String>,
|
||||
|
||||
/// Custom S3 endpoint URL (for MinIO, R2, etc.).
|
||||
#[config(env = "S3_ENDPOINT")]
|
||||
pub s3_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct BackupConfig {
|
||||
/// Enable automatic backups.
|
||||
#[config(env = "BACKUP_ENABLED", default = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Backup storage backend: `filesystem` or `s3`.
|
||||
#[config(env = "BACKUP_STORAGE_BACKEND", default = "filesystem")]
|
||||
pub backend: String,
|
||||
|
||||
/// Path on disk for the filesystem backup backend.
|
||||
#[config(env = "BACKUP_STORAGE_PATH", default = "/var/lib/tranquil-pds/backups")]
|
||||
pub path: String,
|
||||
|
||||
/// S3 bucket name for backups.
|
||||
#[config(env = "BACKUP_S3_BUCKET")]
|
||||
pub s3_bucket: Option<String>,
|
||||
|
||||
/// Number of backup revisions to keep per account.
|
||||
#[config(env = "BACKUP_RETENTION_COUNT", default = 7)]
|
||||
pub retention_count: u32,
|
||||
|
||||
/// Seconds between backup runs.
|
||||
#[config(env = "BACKUP_INTERVAL_SECS", default = 86400)]
|
||||
pub interval_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct CacheConfig {
|
||||
/// Cache backend: `ripple` (default, built-in gossip) or `valkey`.
|
||||
#[config(env = "CACHE_BACKEND", default = "ripple")]
|
||||
pub backend: String,
|
||||
|
||||
/// Valkey / Redis connection URL. Required when `backend = "valkey"`.
|
||||
#[config(env = "VALKEY_URL")]
|
||||
pub valkey_url: Option<String>,
|
||||
|
||||
#[config(nested)]
|
||||
pub ripple: RippleCacheConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct PlcConfig {
|
||||
/// Base URL of the PLC directory.
|
||||
#[config(env = "PLC_DIRECTORY_URL", default = "https://plc.directory")]
|
||||
pub directory_url: String,
|
||||
|
||||
/// HTTP request timeout in seconds.
|
||||
#[config(env = "PLC_TIMEOUT_SECS", default = 10)]
|
||||
pub timeout_secs: u64,
|
||||
|
||||
/// TCP connect timeout in seconds.
|
||||
#[config(env = "PLC_CONNECT_TIMEOUT_SECS", default = 5)]
|
||||
pub connect_timeout_secs: u64,
|
||||
|
||||
/// Seconds to cache DID documents in memory.
|
||||
#[config(env = "DID_CACHE_TTL_SECS", default = 300)]
|
||||
pub did_cache_ttl_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct FirehoseConfig {
|
||||
/// Size of the in-memory broadcast buffer for firehose events.
|
||||
#[config(env = "FIREHOSE_BUFFER_SIZE", default = 10000)]
|
||||
pub buffer_size: usize,
|
||||
|
||||
/// How many hours of historical events to replay for cursor-based
|
||||
/// firehose connections.
|
||||
#[config(env = "FIREHOSE_BACKFILL_HOURS", default = 72)]
|
||||
pub backfill_hours: i64,
|
||||
|
||||
/// Maximum number of lagged events before disconnecting a slow consumer.
|
||||
#[config(env = "FIREHOSE_MAX_LAG", default = 5000)]
|
||||
pub max_lag: u64,
|
||||
|
||||
/// List of relay / crawler notification URLs.
|
||||
#[config(env = "CRAWLERS", parse_env = split_comma_list)]
|
||||
pub crawlers: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FirehoseConfig {
|
||||
/// Returns the list of crawler URLs, falling back to `["https://bsky.network"]`
|
||||
/// when none are configured.
|
||||
pub fn crawler_list(&self) -> Vec<String> {
|
||||
self.crawlers
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec!["https://bsky.network".to_string()])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct EmailConfig {
|
||||
/// Sender email address. When unset, email sending is disabled.
|
||||
#[config(env = "MAIL_FROM_ADDRESS")]
|
||||
pub from_address: Option<String>,
|
||||
|
||||
/// Display name used in the `From` header.
|
||||
#[config(env = "MAIL_FROM_NAME", default = "Tranquil PDS")]
|
||||
pub from_name: String,
|
||||
|
||||
/// Path to the `sendmail` binary.
|
||||
#[config(env = "SENDMAIL_PATH", default = "/usr/sbin/sendmail")]
|
||||
pub sendmail_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct DiscordConfig {
|
||||
/// Discord bot token. When unset, Discord integration is disabled.
|
||||
#[config(env = "DISCORD_BOT_TOKEN")]
|
||||
pub bot_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct TelegramConfig {
|
||||
/// Telegram bot token. When unset, Telegram integration is disabled.
|
||||
#[config(env = "TELEGRAM_BOT_TOKEN")]
|
||||
pub bot_token: Option<String>,
|
||||
|
||||
/// Secret token for incoming webhook verification.
|
||||
#[config(env = "TELEGRAM_WEBHOOK_SECRET")]
|
||||
pub webhook_secret: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SignalConfig {
|
||||
/// Path to the `signal-cli` binary.
|
||||
#[config(env = "SIGNAL_CLI_PATH", default = "/usr/local/bin/signal-cli")]
|
||||
pub cli_path: String,
|
||||
|
||||
/// Sender phone number. When unset, Signal integration is disabled.
|
||||
#[config(env = "SIGNAL_SENDER_NUMBER")]
|
||||
pub sender_number: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct NotificationConfig {
|
||||
/// Polling interval in milliseconds for the comms queue.
|
||||
#[config(env = "NOTIFICATION_POLL_INTERVAL_MS", default = 1000)]
|
||||
pub poll_interval_ms: u64,
|
||||
|
||||
/// Number of notifications to process per batch.
|
||||
#[config(env = "NOTIFICATION_BATCH_SIZE", default = 100)]
|
||||
pub batch_size: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SsoConfig {
|
||||
#[config(nested)]
|
||||
pub github: SsoProviderConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub discord: SsoDiscordProviderConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub google: SsoProviderConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub gitlab: SsoProviderWithIssuerConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub oidc: SsoProviderWithIssuerConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub apple: SsoAppleConfig,
|
||||
}
|
||||
|
||||
// Generic SSO provider (GitHub, Google)
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SsoProviderConfig {
|
||||
#[config(default = false)]
|
||||
pub enabled: bool,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
// SSO provider with custom env prefixes for Discord
|
||||
// (since the nested TOML key is `sso.discord` but env vars are `SSO_DISCORD_*`)
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SsoDiscordProviderConfig {
|
||||
#[config(default = false)]
|
||||
pub enabled: bool,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
// SSO providers that require an issuer URL (GitLab, OIDC)
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SsoProviderWithIssuerConfig {
|
||||
#[config(default = false)]
|
||||
pub enabled: bool,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
pub issuer: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct SsoAppleConfig {
|
||||
#[config(env = "SSO_APPLE_ENABLED", default = false)]
|
||||
pub enabled: bool,
|
||||
|
||||
#[config(env = "SSO_APPLE_CLIENT_ID")]
|
||||
pub client_id: Option<String>,
|
||||
|
||||
#[config(env = "SSO_APPLE_TEAM_ID")]
|
||||
pub team_id: Option<String>,
|
||||
|
||||
#[config(env = "SSO_APPLE_KEY_ID")]
|
||||
pub key_id: Option<String>,
|
||||
|
||||
#[config(env = "SSO_APPLE_PRIVATE_KEY")]
|
||||
pub private_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct ModerationConfig {
|
||||
/// External report-handling service URL.
|
||||
#[config(env = "REPORT_SERVICE_URL")]
|
||||
pub report_service_url: Option<String>,
|
||||
|
||||
/// DID of the external report-handling service.
|
||||
#[config(env = "REPORT_SERVICE_DID")]
|
||||
pub report_service_did: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct ImportConfig {
|
||||
/// Whether the PDS accepts repo imports.
|
||||
#[config(env = "ACCEPTING_REPO_IMPORTS", default = true)]
|
||||
pub accepting: bool,
|
||||
|
||||
/// Maximum allowed import archive size in bytes (default 1 GiB).
|
||||
#[config(env = "MAX_IMPORT_SIZE", default = 1_073_741_824)]
|
||||
pub max_size: u64,
|
||||
|
||||
/// Maximum number of blocks allowed in an import.
|
||||
#[config(env = "MAX_IMPORT_BLOCKS", default = 500000)]
|
||||
pub max_blocks: u64,
|
||||
|
||||
/// Skip CAR verification during import. Only for development/debugging.
|
||||
#[config(env = "SKIP_IMPORT_VERIFICATION", default = false)]
|
||||
pub skip_verification: bool,
|
||||
}
|
||||
|
||||
/// Parse a comma-separated environment variable into a `Vec<String>`,
|
||||
/// trimming whitespace and dropping empty entries.
|
||||
///
|
||||
/// Signature matches confique's `parse_env` expectation: `fn(&str) -> Result<T, E>`.
|
||||
fn split_comma_list(value: &str) -> Result<Vec<String>, std::convert::Infallible> {
|
||||
Ok(value
|
||||
.split(',')
|
||||
.map(|item| item.trim().to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct RippleCacheConfig {
|
||||
/// Address to bind the Ripple gossip protocol listener.
|
||||
#[config(env = "RIPPLE_BIND", default = "0.0.0.0:0")]
|
||||
pub bind_addr: String,
|
||||
|
||||
/// List of seed peer addresses.
|
||||
#[config(env = "RIPPLE_PEERS", parse_env = split_comma_list)]
|
||||
pub peers: Option<Vec<String>>,
|
||||
|
||||
/// Unique machine identifier. Auto-derived from hostname when not set.
|
||||
#[config(env = "RIPPLE_MACHINE_ID")]
|
||||
pub machine_id: Option<u64>,
|
||||
|
||||
/// Gossip protocol interval in milliseconds.
|
||||
#[config(env = "RIPPLE_GOSSIP_INTERVAL_MS", default = 200)]
|
||||
pub gossip_interval_ms: u64,
|
||||
|
||||
/// Maximum cache size in megabytes.
|
||||
#[config(env = "RIPPLE_CACHE_MAX_MB", default = 256)]
|
||||
pub cache_max_mb: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct ScheduledConfig {
|
||||
/// Interval in seconds between scheduled delete checks.
|
||||
#[config(env = "SCHEDULED_DELETE_CHECK_INTERVAL_SECS", default = 3600)]
|
||||
pub delete_check_interval_secs: u64,
|
||||
}
|
||||
|
||||
/// Generate a TOML configuration template with all available options,
|
||||
/// defaults, and documentation comments.
|
||||
pub fn template() -> String {
|
||||
confique::toml::template::<TranquilConfig>(confique::toml::FormatOptions::default())
|
||||
}
|
||||
@@ -5,6 +5,8 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
@@ -45,16 +45,14 @@ pub trait BackupStorage: Send + Sync {
|
||||
}
|
||||
|
||||
pub fn backup_retention_count() -> u32 {
|
||||
std::env::var("BACKUP_RETENTION_COUNT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
tranquil_config::try_get()
|
||||
.map(|c| c.backup.retention_count)
|
||||
.unwrap_or(7)
|
||||
}
|
||||
|
||||
pub fn backup_interval_secs() -> u64 {
|
||||
std::env::var("BACKUP_INTERVAL_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
tranquil_config::try_get()
|
||||
.map(|c| c.backup.interval_secs)
|
||||
.unwrap_or(86400)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-crypto = { workspace = true }
|
||||
tranquil-storage = { workspace = true }
|
||||
tranquil-cache = { workspace = true }
|
||||
@@ -29,6 +30,7 @@ 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 }
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -45,7 +44,7 @@ pub async fn send_email(
|
||||
|
||||
let email = user.email.ok_or(ApiError::NoEmail)?;
|
||||
let (user_id, handle) = (user.id, user.handle);
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let subject = input
|
||||
.subject
|
||||
.clone()
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::api::error::ApiError;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use crate::util::pds_hostname_without_port;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -70,7 +69,7 @@ pub async fn update_account_handle(
|
||||
{
|
||||
return Err(ApiError::InvalidHandle(None));
|
||||
}
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let handle = if !input_handle.contains('.') {
|
||||
format!("{}.{}", input_handle, hostname_for_handles)
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::delegation::{
|
||||
use crate::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use crate::util::{pds_hostname, pds_hostname_without_port};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -435,8 +434,8 @@ pub async fn create_delegated_account(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_handles);
|
||||
|
||||
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
|
||||
@@ -475,7 +474,7 @@ pub async fn create_delegated_account(
|
||||
Err(_) => return Ok(ApiError::InvalidInviteCode.into_response()),
|
||||
}
|
||||
} else {
|
||||
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return Ok(ApiError::InviteCodeRequired.into_response());
|
||||
}
|
||||
@@ -497,8 +496,11 @@ pub async fn create_delegated_account(
|
||||
}
|
||||
};
|
||||
|
||||
let rotation_key = std::env::var("PLC_ROTATION_KEY")
|
||||
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
|
||||
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,
|
||||
|
||||
@@ -12,7 +12,7 @@ use tranquil_types::Handle;
|
||||
|
||||
use crate::comms::comms_repo;
|
||||
use crate::state::AppState;
|
||||
use crate::util::{discord_public_key, pds_hostname};
|
||||
use crate::util::discord_public_key;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Interaction {
|
||||
@@ -185,7 +185,7 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response {
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Discord,
|
||||
&discord_user_id,
|
||||
pds_hostname(),
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ 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::util::{pds_hostname, pds_hostname_without_port};
|
||||
use crate::validation::validate_password;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -141,7 +140,7 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
|
||||
let hostname_for_validation = pds_hostname_without_port();
|
||||
let hostname_for_validation = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_validation);
|
||||
|
||||
let validated_short_handle = if !input.handle.contains('.')
|
||||
@@ -233,8 +232,8 @@ pub async fn create_account(
|
||||
},
|
||||
})
|
||||
};
|
||||
let hostname = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let suffix = format!(".{}", hostname_for_handles);
|
||||
let handle = if input.handle.ends_with(&suffix) {
|
||||
@@ -326,8 +325,11 @@ pub async fn create_account(
|
||||
)
|
||||
.into_response();
|
||||
} else {
|
||||
let rotation_key = std::env::var("PLC_ROTATION_KEY")
|
||||
.unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
|
||||
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,
|
||||
@@ -359,8 +361,11 @@ pub async fn create_account(
|
||||
genesis_result.did
|
||||
}
|
||||
} else {
|
||||
let rotation_key = std::env::var("PLC_ROTATION_KEY")
|
||||
.unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
|
||||
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,
|
||||
@@ -473,7 +478,7 @@ pub async fn create_account(
|
||||
error!("Error creating session: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let hostname = pds_hostname();
|
||||
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(
|
||||
&did_typed, user_email,
|
||||
@@ -543,7 +548,7 @@ pub async fn create_account(
|
||||
return ApiError::HandleTaken.into_response();
|
||||
}
|
||||
|
||||
let invite_code_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
|
||||
let invite_code_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_code_required
|
||||
&& input
|
||||
.invite_code
|
||||
@@ -632,12 +637,14 @@ pub async fn create_account(
|
||||
let rev_str = rev.as_ref().to_string();
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
|
||||
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
|
||||
json!({
|
||||
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
|
||||
Some(json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
})
|
||||
});
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let preferred_comms_channel = verification_channel;
|
||||
|
||||
@@ -748,7 +755,7 @@ pub async fn create_account(
|
||||
warn!("Failed to create default profile for {}: {}", did, e);
|
||||
}
|
||||
}
|
||||
let hostname = pds_hostname();
|
||||
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(
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::rate_limit::{
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Handle;
|
||||
use crate::util::{get_header_str, pds_hostname, pds_hostname_without_port};
|
||||
use crate::util::get_header_str;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
@@ -122,8 +122,8 @@ pub fn get_public_key_multibase(key_bytes: &[u8]) -> Result<String, KeyError> {
|
||||
}
|
||||
|
||||
pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
let hostname = pds_hostname();
|
||||
let hostname_without_port = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_without_port = tranquil_config::get().server.hostname_without_port();
|
||||
let host_header = get_header_str(&headers, http::header::HOST).unwrap_or(hostname);
|
||||
let host_without_port = host_header.split(':').next().unwrap_or(host_header);
|
||||
if host_without_port != hostname_without_port
|
||||
@@ -275,8 +275,8 @@ async fn serve_subdomain_did_doc(state: &AppState, subdomain: &str, hostname: &s
|
||||
}
|
||||
|
||||
pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
|
||||
let hostname = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let current_handle = format!("{}.{}", handle, hostname_for_handles);
|
||||
let current_handle_typed: Handle = match current_handle.parse() {
|
||||
Ok(h) => h,
|
||||
@@ -571,7 +571,7 @@ pub async fn get_recommended_did_credentials(
|
||||
ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into()))
|
||||
})?;
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)
|
||||
.map_err(|_| ApiError::InternalError(None))?;
|
||||
@@ -579,9 +579,9 @@ pub async fn get_recommended_did_credentials(
|
||||
let rotation_keys = if auth.did.starts_with("did:web:") {
|
||||
vec![]
|
||||
} else {
|
||||
let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
let server_rotation_key = match &tranquil_config::get().secrets.plc_rotation_key {
|
||||
Some(key) => key.clone(),
|
||||
None => {
|
||||
warn!(
|
||||
"PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"
|
||||
);
|
||||
@@ -675,7 +675,7 @@ pub async fn update_handle(
|
||||
"Inappropriate language in handle".into(),
|
||||
)));
|
||||
}
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let suffix = format!(".{}", hostname_for_handles);
|
||||
let is_service_domain =
|
||||
crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles);
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Auth, Permissive};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -41,7 +40,7 @@ pub async fn request_plc_operation_signature(
|
||||
.await
|
||||
.log_db_err("creating PLC token")?;
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -42,7 +41,7 @@ pub async fn submit_plc_operation(
|
||||
.map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?;
|
||||
|
||||
let op = &input.operation;
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let public_url = format!("https://{}", hostname);
|
||||
let user = state
|
||||
.user_repo
|
||||
@@ -70,8 +69,11 @@ pub async fn submit_plc_operation(
|
||||
})?;
|
||||
|
||||
let user_did_key = signing_key_to_did_key(&signing_key);
|
||||
let server_rotation_key =
|
||||
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
|
||||
let server_rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| user_did_key.clone());
|
||||
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
|
||||
let has_server_key = rotation_keys
|
||||
.iter()
|
||||
|
||||
@@ -69,8 +69,9 @@ struct ReportServiceConfig {
|
||||
}
|
||||
|
||||
fn get_report_service_config() -> Option<ReportServiceConfig> {
|
||||
let url = std::env::var("REPORT_SERVICE_URL").ok()?;
|
||||
let did = std::env::var("REPORT_SERVICE_DID").ok()?;
|
||||
let cfg = tranquil_config::get();
|
||||
let url = cfg.moderation.report_service_url.clone()?;
|
||||
let did = cfg.moderation.report_service_did.clone()?;
|
||||
if url.is_empty() || did.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -148,7 +147,7 @@ pub async fn request_channel_verification(
|
||||
|
||||
match channel {
|
||||
CommsChannel::Email => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let handle_str = handle.unwrap_or("user");
|
||||
crate::comms::comms_repo::enqueue_email_update(
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -167,7 +166,7 @@ pub async fn request_channel_verification(
|
||||
})?;
|
||||
}
|
||||
_ => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let encoded_token = urlencoding::encode(&formatted_token);
|
||||
let encoded_identifier = urlencoding::encode(identifier);
|
||||
let verify_link = format!(
|
||||
|
||||
@@ -168,7 +168,7 @@ impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Reque
|
||||
}
|
||||
|
||||
// If the age assurance override is set and this is an age assurance call then we dont want to proxy even if the client requests it
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok()
|
||||
if tranquil_config::get().server.age_assurance_override
|
||||
&& (path.ends_with("app.bsky.ageassurance.getState")
|
||||
|| path.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn is_ssrf_safe(url: &str) -> Result<(), SsrfError> {
|
||||
let parsed = Url::parse(url).map_err(|_| SsrfError::InvalidUrl)?;
|
||||
let scheme = parsed.scheme();
|
||||
if scheme != "https" {
|
||||
let allow_http = std::env::var("ALLOW_HTTP_PROXY").is_ok()
|
||||
let allow_http = tranquil_config::get().server.allow_http_proxy
|
||||
|| url.starts_with("http://127.0.0.1")
|
||||
|| url.starts_with("http://localhost");
|
||||
if !allow_http {
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, get_max_blob_size};
|
||||
use crate::util::get_header_str;
|
||||
use axum::body::Body;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -89,7 +89,7 @@ pub async fn upload_blob(
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
|
||||
let temp_key = format!("temp/{}", uuid::Uuid::new_v4());
|
||||
let max_size = u64::try_from(get_max_blob_size()).unwrap_or(u64::MAX);
|
||||
let max_size = tranquil_config::get().server.max_blob_size;
|
||||
|
||||
let body_stream = body.into_data_stream();
|
||||
let mapped_stream =
|
||||
|
||||
@@ -18,26 +18,18 @@ use serde_json::json;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tranquil_types::{AtUri, CidLink};
|
||||
|
||||
const DEFAULT_MAX_IMPORT_SIZE: usize = 1024 * 1024 * 1024;
|
||||
const DEFAULT_MAX_BLOCKS: usize = 500000;
|
||||
|
||||
pub async fn import_repo(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<NotTakendown>,
|
||||
body: Bytes,
|
||||
) -> Result<Response, ApiError> {
|
||||
let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS")
|
||||
.map(|v| v != "false" && v != "0")
|
||||
.unwrap_or(true);
|
||||
let accepting_imports = tranquil_config::get().import.accepting;
|
||||
if !accepting_imports {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Service is not accepting repo imports".into(),
|
||||
));
|
||||
}
|
||||
let max_size: usize = std::env::var("MAX_IMPORT_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_IMPORT_SIZE);
|
||||
let max_size = tranquil_config::get().import.max_size as usize;
|
||||
if body.len() > max_size {
|
||||
return Err(ApiError::PayloadTooLarge(format!(
|
||||
"Import size exceeds limit of {} bytes",
|
||||
@@ -108,7 +100,7 @@ pub async fn import_repo(
|
||||
commit_did, did
|
||||
)));
|
||||
}
|
||||
let skip_verification = crate::util::parse_env_bool("SKIP_IMPORT_VERIFICATION");
|
||||
let skip_verification = tranquil_config::get().import.skip_verification;
|
||||
let is_migration = user.deactivated_at.is_some();
|
||||
if skip_verification {
|
||||
warn!("Skipping all CAR verification for import (SKIP_IMPORT_VERIFICATION=true)");
|
||||
@@ -196,10 +188,7 @@ pub async fn import_repo(
|
||||
}
|
||||
}
|
||||
}
|
||||
let max_blocks: usize = std::env::var("MAX_IMPORT_BLOCKS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_BLOCKS);
|
||||
let max_blocks = tranquil_config::get().import.max_blocks as usize;
|
||||
let _write_lock = state.repo_write_locks.lock(user_id).await;
|
||||
match apply_import(
|
||||
&state.repo_repo,
|
||||
@@ -324,7 +313,7 @@ pub async fn import_repo(
|
||||
{
|
||||
warn!("Failed to sequence import event: {:?}", e);
|
||||
}
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() {
|
||||
if tranquil_config::get().server.age_assurance_override {
|
||||
let birthdate_pref = json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::types::AtIdentifier;
|
||||
use crate::util::pds_hostname_without_port;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -19,7 +18,7 @@ pub async fn describe_repo(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<DescribeRepoInput>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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() {
|
||||
Ok(d) => d,
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::pagination::{PaginationDirection, deserialize_pagination_direction};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AtIdentifier, Nsid, Rkey};
|
||||
use crate::util::pds_hostname_without_port;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -60,7 +59,7 @@ pub async fn get_record(
|
||||
_headers: HeaderMap,
|
||||
Query(input): Query<GetRecordInput>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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() {
|
||||
Ok(d) => d,
|
||||
@@ -159,7 +158,7 @@ pub async fn list_records(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<ListRecordsInput>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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() {
|
||||
Ok(d) => d,
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::cache::Cache;
|
||||
use crate::plc::PlcClient;
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -131,7 +130,7 @@ async fn assert_valid_did_document_for_service(
|
||||
did: &crate::types::Did,
|
||||
with_retry: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let expected_endpoint = format!("https://{}", hostname);
|
||||
|
||||
if did.as_str().starts_with("did:plc:") {
|
||||
@@ -201,7 +200,7 @@ async fn assert_valid_did_document_for_service(
|
||||
.await
|
||||
.map_err(ApiError::InvalidRequest)?;
|
||||
|
||||
let server_rotation_key = std::env::var("PLC_ROTATION_KEY").ok();
|
||||
let server_rotation_key = tranquil_config::get().secrets.plc_rotation_key.clone();
|
||||
if let Some(ref expected_rotation_key) = server_rotation_key {
|
||||
let rotation_keys = doc_data
|
||||
.get("rotationKeys")
|
||||
@@ -552,7 +551,7 @@ pub async fn request_account_delete(
|
||||
.create_deletion_request(&confirmation_token, session_mfa.did(), expires_at)
|
||||
.await
|
||||
.log_db_err("creating deletion token")?;
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -105,7 +104,7 @@ pub async fn request_email_update(
|
||||
}
|
||||
}
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_short_token_email(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -367,7 +366,7 @@ pub async fn update_email(
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -531,7 +530,7 @@ pub async fn authorize_email_update(
|
||||
|
||||
info!(did = %did, "Email update authorized via link click");
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let redirect_url = format!(
|
||||
"https://{}/app/verify?type=email-authorize-success",
|
||||
hostname
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::api::error::DbResultExt;
|
||||
use crate::auth::{Admin, Auth, NotTakendown};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -26,7 +25,7 @@ fn gen_random_token() -> String {
|
||||
}
|
||||
|
||||
fn gen_invite_code() -> String {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_prefix = hostname.replace('.', "-");
|
||||
format!("{}-{}", hostname_prefix, gen_random_token())
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use crate::state::AppState;
|
||||
use crate::util::{discord_app_id, discord_bot_username, pds_hostname, telegram_bot_username};
|
||||
use crate::util::{discord_app_id, discord_bot_username, telegram_bot_username};
|
||||
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
|
||||
use serde_json::json;
|
||||
|
||||
fn get_available_comms_channels() -> Vec<tranquil_db_traits::CommsChannel> {
|
||||
use tranquil_db_traits::CommsChannel;
|
||||
let cfg = tranquil_config::get();
|
||||
let mut channels = vec![CommsChannel::Email];
|
||||
if std::env::var("DISCORD_BOT_TOKEN").is_ok() {
|
||||
if cfg.discord.bot_token.is_some() {
|
||||
channels.push(CommsChannel::Discord);
|
||||
}
|
||||
if std::env::var("TELEGRAM_BOT_TOKEN").is_ok() {
|
||||
if cfg.telegram.bot_token.is_some() {
|
||||
channels.push(CommsChannel::Telegram);
|
||||
}
|
||||
if std::env::var("SIGNAL_CLI_PATH").is_ok() && std::env::var("SIGNAL_SENDER_NUMBER").is_ok() {
|
||||
if cfg.signal.sender_number.is_some() {
|
||||
channels.push(CommsChannel::Signal);
|
||||
}
|
||||
channels
|
||||
@@ -26,20 +27,17 @@ pub async fn robots_txt() -> impl IntoResponse {
|
||||
)
|
||||
}
|
||||
pub fn is_self_hosted_did_web_enabled() -> bool {
|
||||
std::env::var("ENABLE_SELF_HOSTED_DID_WEB")
|
||||
.map(|v| v != "false" && v != "0")
|
||||
.unwrap_or(true)
|
||||
tranquil_config::get().server.enable_pds_hosted_did_web
|
||||
}
|
||||
|
||||
pub async fn describe_server() -> impl IntoResponse {
|
||||
let pds_hostname = pds_hostname();
|
||||
let domains_str =
|
||||
std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| pds_hostname.to_string());
|
||||
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
|
||||
let invite_code_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
|
||||
let privacy_policy = std::env::var("PRIVACY_POLICY_URL").ok();
|
||||
let terms_of_service = std::env::var("TERMS_OF_SERVICE_URL").ok();
|
||||
let contact_email = std::env::var("CONTACT_EMAIL").ok();
|
||||
let cfg = tranquil_config::get();
|
||||
let pds_hostname = &cfg.server.hostname;
|
||||
let domains = cfg.server.available_user_domain_list();
|
||||
let invite_code_required = cfg.server.invite_code_required;
|
||||
let privacy_policy = cfg.server.privacy_policy_url.clone();
|
||||
let terms_of_service = cfg.server.terms_of_service_url.clone();
|
||||
let contact_email = cfg.server.contact_email.clone();
|
||||
let mut links = serde_json::Map::new();
|
||||
if let Some(pp) = privacy_policy {
|
||||
links.insert("privacyPolicy".to_string(), json!(pp));
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::api::ApiError;
|
||||
use crate::api::error::DbResultExt;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -147,7 +146,7 @@ pub async fn get_did_document(
|
||||
}
|
||||
|
||||
async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
|
||||
let user = match state.user_repo.get_user_for_did_doc_build(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
|
||||
@@ -24,7 +24,6 @@ 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::util::{pds_hostname, pds_hostname_without_port};
|
||||
use crate::validation::validate_password;
|
||||
|
||||
fn generate_setup_token() -> String {
|
||||
@@ -113,8 +112,8 @@ pub async fn create_passkey_account(
|
||||
.map(|d| d.starts_with("did:web:"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_handles);
|
||||
|
||||
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
|
||||
@@ -153,7 +152,7 @@ pub async fn create_passkey_account(
|
||||
Err(_) => return ApiError::InvalidInviteCode.into_response(),
|
||||
}
|
||||
} else {
|
||||
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return ApiError::InviteCodeRequired.into_response();
|
||||
}
|
||||
@@ -309,8 +308,11 @@ pub async fn create_passkey_account(
|
||||
.into_response();
|
||||
}
|
||||
} else {
|
||||
let rotation_key = std::env::var("PLC_ROTATION_KEY")
|
||||
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&secret_key));
|
||||
let rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&secret_key));
|
||||
|
||||
let genesis_result = match crate::plc::create_genesis_operation(
|
||||
&secret_key,
|
||||
@@ -401,12 +403,14 @@ pub async fn create_passkey_account(
|
||||
};
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
|
||||
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
|
||||
json!({
|
||||
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
|
||||
Some(json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
})
|
||||
});
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let handle_typed: Handle = match handle.parse() {
|
||||
Ok(h) => h,
|
||||
@@ -820,7 +824,7 @@ pub async fn request_passkey_recovery(
|
||||
_rate_limit: RateLimited<PasswordResetLimit>,
|
||||
Json(input): Json<RequestPasskeyRecoveryInput>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let identifier = input.email.trim().to_lowercase();
|
||||
let identifier = identifier.strip_prefix('@').unwrap_or(&identifier);
|
||||
let normalized_handle =
|
||||
@@ -855,7 +859,7 @@ pub async fn request_passkey_recovery(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let recovery_url = format!(
|
||||
"https://{}/app/recover-passkey?did={}&token={}",
|
||||
hostname,
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::auth::{
|
||||
use crate::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit};
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use crate::util::{pds_hostname, pds_hostname_without_port};
|
||||
use crate::validation::validate_password;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -38,7 +37,7 @@ pub async fn request_password_reset(
|
||||
if identifier.is_empty() {
|
||||
return ApiError::InvalidRequest("email or handle is required".into()).into_response();
|
||||
}
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized = identifier.to_lowercase();
|
||||
let normalized = normalized.strip_prefix('@').unwrap_or(&normalized);
|
||||
let is_email_lookup = normalized.contains('@');
|
||||
@@ -78,7 +77,7 @@ pub async fn request_password_reset(
|
||||
error!("DB error setting reset code: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_password_reset(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::auth::{
|
||||
use crate::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AccountState, Did, Handle, PlainPassword};
|
||||
use crate::util::{pds_hostname, pds_hostname_without_port};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -66,10 +65,10 @@ pub async fn create_session(
|
||||
"create_session called with identifier: {}",
|
||||
input.identifier
|
||||
);
|
||||
let pds_host = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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
|
||||
@@ -182,7 +181,7 @@ pub async fn create_session(
|
||||
return ApiError::LegacyLoginBlocked.into_response();
|
||||
}
|
||||
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -286,7 +285,7 @@ pub async fn create_session(
|
||||
ip = %client_ip,
|
||||
"Legacy login on TOTP-enabled account - sending notification"
|
||||
);
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -341,7 +340,7 @@ pub async fn get_session(
|
||||
let preferred_channel_verified = row
|
||||
.channel_verification
|
||||
.is_verified(row.preferred_comms_channel);
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let handle = full_handle(&row.handle, pds_hostname);
|
||||
let account_state = AccountState::from_db_fields(
|
||||
row.deactivated_at,
|
||||
@@ -545,7 +544,7 @@ pub async fn refresh_session(
|
||||
let preferred_channel_verified = u
|
||||
.channel_verification
|
||||
.is_verified(u.preferred_comms_channel);
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let handle = full_handle(&u.handle, pds_hostname);
|
||||
let account_state =
|
||||
AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None);
|
||||
@@ -707,7 +706,7 @@ pub async fn confirm_signup(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -777,7 +776,7 @@ pub async fn resend_verification(
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::auth::{
|
||||
use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -52,7 +51,7 @@ pub async fn create_totp_secret(
|
||||
.log_db_err("fetching handle")?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let uri = generate_totp_uri(&secret, &handle, hostname);
|
||||
|
||||
let qr_code = generate_qr_png_base64(&secret, &handle, hostname).map_err(|e| {
|
||||
|
||||
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -71,7 +70,7 @@ pub async fn resend_migration_verification(
|
||||
return Ok(Json(ResendMigrationVerificationOutput { sent: true }));
|
||||
}
|
||||
|
||||
let hostname = pds_hostname();
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::comms::comms_repo;
|
||||
use crate::types::Did;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
@@ -162,7 +161,7 @@ async fn handle_channel_update(
|
||||
user_id,
|
||||
channel,
|
||||
&recipient,
|
||||
pds_hostname(),
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -260,7 +259,7 @@ async fn handle_signup_verification(
|
||||
user.id,
|
||||
channel,
|
||||
&recipient,
|
||||
pds_hostname(),
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ use tracing::{debug, info, warn};
|
||||
|
||||
use crate::comms::comms_repo;
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TelegramUpdate {
|
||||
@@ -32,9 +31,9 @@ pub async fn handle_telegram_webhook(
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> impl IntoResponse {
|
||||
let expected_secret = match std::env::var("TELEGRAM_WEBHOOK_SECRET") {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
let expected_secret = match &tranquil_config::get().telegram.webhook_secret {
|
||||
Some(s) => s.clone(),
|
||||
None => {
|
||||
warn!("Telegram webhook called but TELEGRAM_WEBHOOK_SECRET is not configured");
|
||||
return StatusCode::FORBIDDEN;
|
||||
}
|
||||
@@ -88,7 +87,7 @@ pub async fn handle_telegram_webhook(
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Telegram,
|
||||
&from.id.to_string(),
|
||||
pds_hostname(),
|
||||
&tranquil_config::get().server.hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -64,13 +64,10 @@ pub struct DidResolver {
|
||||
|
||||
impl DidResolver {
|
||||
pub fn new() -> Self {
|
||||
let cache_ttl_secs: u64 = std::env::var("DID_CACHE_TTL_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(300);
|
||||
let cfg = tranquil_config::get();
|
||||
let cache_ttl_secs = cfg.plc.did_cache_ttl_secs;
|
||||
|
||||
let plc_directory_url = std::env::var("PLC_DIRECTORY_URL")
|
||||
.unwrap_or_else(|_| "https://plc.directory".to_string());
|
||||
let plc_directory_url = cfg.plc.directory_url.clone();
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::util::pds_hostname;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::Utc;
|
||||
@@ -146,10 +145,9 @@ pub struct ServiceTokenVerifier {
|
||||
|
||||
impl ServiceTokenVerifier {
|
||||
pub fn new() -> Self {
|
||||
let plc_directory_url = std::env::var("PLC_DIRECTORY_URL")
|
||||
.unwrap_or_else(|_| "https://plc.directory".to_string());
|
||||
let plc_directory_url = tranquil_config::get().plc.directory_url.clone();
|
||||
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let pds_did: Did = format!("did:web:{}", pds_hostname)
|
||||
.parse()
|
||||
.expect("PDS hostname produces a valid DID");
|
||||
|
||||
@@ -61,13 +61,7 @@ pub struct VerificationToken {
|
||||
|
||||
fn derive_verification_key() -> [u8; 32] {
|
||||
use hkdf::Hkdf;
|
||||
let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-master-key-not-for-production".to_string()
|
||||
} else {
|
||||
panic!("MASTER_KEY must be set");
|
||||
}
|
||||
});
|
||||
let master_key = tranquil_config::get().secrets.master_key_or_default();
|
||||
let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
|
||||
let mut key = [0u8; 32];
|
||||
hk.expand(b"tranquil-pds-verification-token-v1", &mut key)
|
||||
|
||||
@@ -21,14 +21,9 @@ pub struct CommsService {
|
||||
|
||||
impl CommsService {
|
||||
pub fn new(infra_repo: Arc<dyn InfraRepository>) -> Self {
|
||||
let poll_interval_ms: u64 = std::env::var("NOTIFICATION_POLL_INTERVAL_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(1000);
|
||||
let batch_size: i64 = std::env::var("NOTIFICATION_BATCH_SIZE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(100);
|
||||
let cfg = tranquil_config::get();
|
||||
let poll_interval_ms = cfg.notifications.poll_interval_ms;
|
||||
let batch_size = cfg.notifications.batch_size;
|
||||
Self {
|
||||
infra_repo,
|
||||
senders: HashMap::new(),
|
||||
|
||||
@@ -48,39 +48,10 @@ pub struct AuthConfig {
|
||||
impl AuthConfig {
|
||||
pub fn init() -> &'static Self {
|
||||
CONFIG.get_or_init(|| {
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-jwt-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"JWT_SECRET environment variable must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
let secrets = &tranquil_config::get().secrets;
|
||||
|
||||
let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-dpop-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"DPOP_SECRET environment variable must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if jwt_secret.len() < 32
|
||||
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
|
||||
{
|
||||
panic!("JWT_SECRET must be at least 32 characters");
|
||||
}
|
||||
|
||||
if dpop_secret.len() < 32
|
||||
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
|
||||
{
|
||||
panic!("DPOP_SECRET must be at least 32 characters");
|
||||
}
|
||||
let jwt_secret = secrets.jwt_secret_or_default();
|
||||
let dpop_secret = secrets.dpop_secret_or_default();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"oauth-signing-key-derivation:");
|
||||
@@ -114,22 +85,7 @@ impl AuthConfig {
|
||||
let kid_hash = kid_hasher.finalize();
|
||||
let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
|
||||
|
||||
let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-master-key-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"MASTER_KEY environment variable must be set in production. \
|
||||
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if master_key.len() < 32
|
||||
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
|
||||
{
|
||||
panic!("MASTER_KEY must be at least 32 characters");
|
||||
}
|
||||
let master_key = secrets.master_key_or_default();
|
||||
|
||||
let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
|
||||
let mut key_encryption_key = [0u8; 32];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::circuit_breaker::CircuitBreaker;
|
||||
use crate::sync::firehose::SequencedEvent;
|
||||
use crate::util::pds_hostname;
|
||||
use reqwest::Client;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -42,18 +41,13 @@ impl Crawlers {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let hostname = pds_hostname();
|
||||
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
|
||||
let hostname = &cfg.server.hostname;
|
||||
if hostname == "localhost" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let crawler_urls: Vec<String> = std::env::var("CRAWLERS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect();
|
||||
let crawler_urls = cfg.firehose.crawler_list();
|
||||
|
||||
if crawler_urls.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -87,13 +87,11 @@ pub async fn verify_handle_ownership(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
|
||||
pub fn is_service_domain_handle(handle: &str, _hostname: &str) -> bool {
|
||||
if !handle.contains('.') {
|
||||
return true;
|
||||
}
|
||||
let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
|
||||
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
|
||||
.unwrap_or_else(|_| vec![hostname.to_string()]);
|
||||
let service_domains = tranquil_config::get().server.user_handle_domain_list();
|
||||
service_domains
|
||||
.iter()
|
||||
.any(|domain| handle.ends_with(&format!(".{}", domain)) || handle == domain)
|
||||
|
||||
@@ -658,7 +658,9 @@ pub fn app(state: AppState) -> Router {
|
||||
post(api::discord_webhook::handle_discord_webhook)
|
||||
.layer(DefaultBodyLimit::max(64 * 1024)),
|
||||
)
|
||||
.layer(DefaultBodyLimit::max(util::get_max_blob_size()))
|
||||
.layer(DefaultBodyLimit::max(
|
||||
tranquil_config::get().server.max_blob_size as usize,
|
||||
))
|
||||
.layer(axum::middleware::map_response(rewrite_422_to_400))
|
||||
.layer(middleware::from_fn(metrics::metrics_middleware))
|
||||
.layer(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -18,10 +20,82 @@ use tranquil_pds::scheduled::{
|
||||
};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")]
|
||||
struct Cli {
|
||||
/// Path to a TOML configuration file (also settable via TRANQUIL_PDS_CONFIG env var)
|
||||
#[arg(short, long, value_name = "FILE", env = "TRANQUIL_PDS_CONFIG")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Validate the configuration and exit
|
||||
Validate {
|
||||
/// Skip validation of secrets and database URL (useful when secrets
|
||||
/// are provided at runtime via environment variables / secret files)
|
||||
#[arg(long)]
|
||||
ignore_secrets: bool,
|
||||
},
|
||||
/// Print a TOML configuration template to stdout
|
||||
ConfigTemplate,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Handle subcommands that don't need full startup
|
||||
if let Some(command) = &cli.command {
|
||||
return match command {
|
||||
Command::ConfigTemplate => {
|
||||
print!("{}", tranquil_config::template());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Command::Validate { ignore_secrets } => {
|
||||
let config = match tranquil_config::load(cli.config.as_ref()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to load configuration: {e:#}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
match config.validate(*ignore_secrets) {
|
||||
Ok(()) => {
|
||||
println!("Configuration is valid.");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprint!("{e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let config = match tranquil_config::load(cli.config.as_ref()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to load configuration: {e:#}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = config.validate(false) {
|
||||
error!("{e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
tranquil_config::init(config);
|
||||
|
||||
tranquil_pds::metrics::init_metrics();
|
||||
|
||||
match run().await {
|
||||
@@ -66,14 +140,16 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut comms_service = CommsService::new(state.infra_repo.clone());
|
||||
let mut deferred_discord_endpoint: Option<(DiscordSender, String, String)> = None;
|
||||
|
||||
if let Some(email_sender) = EmailSender::from_env() {
|
||||
let cfg = tranquil_config::get();
|
||||
|
||||
if let Some(email_sender) = EmailSender::from_config(cfg) {
|
||||
info!("Email comms enabled");
|
||||
comms_service = comms_service.register_sender(email_sender);
|
||||
} else {
|
||||
warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)");
|
||||
}
|
||||
|
||||
if let Some(discord_sender) = DiscordSender::from_env() {
|
||||
if let Some(discord_sender) = DiscordSender::from_config(cfg) {
|
||||
info!("Discord comms enabled");
|
||||
match discord_sender.resolve_bot_username().await {
|
||||
Ok(username) => {
|
||||
@@ -96,8 +172,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Some(public_key) => {
|
||||
tranquil_pds::util::set_discord_public_key(public_key);
|
||||
info!("Discord Ed25519 public key loaded");
|
||||
let hostname = std::env::var("PDS_HOSTNAME")
|
||||
.unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let webhook_url = format!("https://{}/webhook/discord", hostname);
|
||||
match discord_sender.register_slash_command(&app_id).await {
|
||||
Ok(()) => info!("Discord /start slash command registered"),
|
||||
@@ -118,22 +193,19 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
comms_service = comms_service.register_sender(discord_sender);
|
||||
}
|
||||
|
||||
if let Some(telegram_sender) = TelegramSender::from_env() {
|
||||
let secret_token = match std::env::var("TELEGRAM_WEBHOOK_SECRET") {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err(
|
||||
"TELEGRAM_BOT_TOKEN is set but TELEGRAM_WEBHOOK_SECRET is missing. Both are required for secure Telegram integration.".into()
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some(telegram_sender) = TelegramSender::from_config(cfg) {
|
||||
// Safe to unwrap: validated in TranquilConfig::validate()
|
||||
let secret_token = tranquil_config::get()
|
||||
.telegram
|
||||
.webhook_secret
|
||||
.clone()
|
||||
.expect("telegram.webhook_secret checked during config validation");
|
||||
info!("Telegram comms enabled");
|
||||
match telegram_sender.resolve_bot_username().await {
|
||||
Ok(username) => {
|
||||
info!(bot_username = %username, "Resolved Telegram bot username");
|
||||
tranquil_pds::util::set_telegram_bot_username(username);
|
||||
let hostname =
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname = tranquil_config::get().server.hostname.clone();
|
||||
let webhook_url = format!("https://{}/webhook/telegram", hostname);
|
||||
match telegram_sender
|
||||
.set_webhook(&webhook_url, Some(&secret_token))
|
||||
@@ -150,14 +222,14 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
comms_service = comms_service.register_sender(telegram_sender);
|
||||
}
|
||||
|
||||
if let Some(signal_sender) = SignalSender::from_env() {
|
||||
if let Some(signal_sender) = SignalSender::from_config(cfg) {
|
||||
info!("Signal comms enabled");
|
||||
comms_service = comms_service.register_sender(signal_sender);
|
||||
}
|
||||
|
||||
let comms_handle = tokio::spawn(comms_service.run(shutdown.clone()));
|
||||
|
||||
let crawlers_handle = if let Some(crawlers) = Crawlers::from_env() {
|
||||
let crawlers_handle = if let Some(crawlers) = Crawlers::from_config(cfg) {
|
||||
let crawlers = Arc::new(
|
||||
crawlers.with_circuit_breaker(state.circuit_breakers.relay_notification.clone()),
|
||||
);
|
||||
@@ -197,11 +269,9 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let app = tranquil_pds::app(state);
|
||||
|
||||
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let port: u16 = std::env::var("SERVER_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(3000);
|
||||
let cfg = tranquil_config::get();
|
||||
let host = &cfg.server.host;
|
||||
let port = cfg.server.port;
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", host, port)
|
||||
.parse()
|
||||
|
||||
@@ -34,14 +34,7 @@ fn get_slur_regexes() -> &'static Vec<Regex> {
|
||||
}
|
||||
|
||||
fn get_extra_banned_words() -> &'static Vec<String> {
|
||||
EXTRA_BANNED_WORDS.get_or_init(|| {
|
||||
std::env::var("PDS_BANNED_WORDS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
})
|
||||
EXTRA_BANNED_WORDS.get_or_init(|| tranquil_config::get().server.banned_word_list())
|
||||
}
|
||||
|
||||
fn strip_trailing_digits(s: &str) -> &str {
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::rate_limit::{
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use crate::util::{extract_client_ip, pds_hostname, pds_hostname_without_port};
|
||||
use crate::util::extract_client_ip;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -253,8 +253,8 @@ 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 = pds_hostname_without_port();
|
||||
let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles);
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized = NormalizedLoginIdentifier::normalize(login_hint, &hostname_for_handles);
|
||||
tracing::info!(normalized = %normalized, "Normalized login_hint");
|
||||
|
||||
match state
|
||||
@@ -526,13 +526,13 @@ pub async fn authorize_post(
|
||||
url_encode(error_msg)
|
||||
))
|
||||
};
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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,
|
||||
pds_hostname = %pds_hostname(),
|
||||
pds_hostname = %tranquil_config::get().server.hostname,
|
||||
"Normalized username for lookup"
|
||||
);
|
||||
let user = match state
|
||||
@@ -677,7 +677,7 @@ pub async fn authorize_post(
|
||||
.await
|
||||
{
|
||||
Ok(challenge) => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -992,7 +992,7 @@ pub async fn authorize_select(
|
||||
.await
|
||||
{
|
||||
Ok(challenge) => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -1116,7 +1116,7 @@ fn build_success_redirect(
|
||||
'?'
|
||||
};
|
||||
redirect_url.push(separator);
|
||||
let pds_host = pds_hostname();
|
||||
let pds_host = &tranquil_config::get().server.hostname;
|
||||
redirect_url.push_str(&format!(
|
||||
"iss={}",
|
||||
url_encode(&format!("https://{}", pds_host))
|
||||
@@ -1134,7 +1134,7 @@ fn build_intermediate_redirect_url(
|
||||
state: Option<&str>,
|
||||
response_mode: Option<&str>,
|
||||
) -> String {
|
||||
let pds_host = pds_hostname();
|
||||
let pds_host = &tranquil_config::get().server.hostname;
|
||||
let mut url = format!(
|
||||
"https://{}/oauth/authorize/redirect?redirect_uri={}&code={}",
|
||||
pds_host,
|
||||
@@ -1991,9 +1991,9 @@ pub async fn check_user_has_passkeys(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<CheckPasskeysQuery>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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
|
||||
@@ -2023,9 +2023,9 @@ pub async fn check_user_security_status(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<CheckPasskeysQuery>,
|
||||
) -> Response {
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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
|
||||
@@ -2131,9 +2131,9 @@ pub async fn passkey_start(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
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
|
||||
@@ -2602,7 +2602,7 @@ pub async fn passkey_finish(
|
||||
.await
|
||||
{
|
||||
Ok(challenge) => {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
@@ -2881,7 +2881,7 @@ pub async fn authorize_passkey_finish(
|
||||
headers: HeaderMap,
|
||||
Json(form): Json<AuthorizePasskeySubmit>,
|
||||
) -> Response {
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let passkey_finish_request_id = RequestId::from(form.request_uri.clone());
|
||||
|
||||
let request_data = match state
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::oauth::jwks::{JwkSet, create_jwk_set};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -58,7 +57,7 @@ pub struct AuthorizationServerMetadata {
|
||||
pub async fn oauth_protected_resource(
|
||||
State(_state): State<AppState>,
|
||||
) -> Json<ProtectedResourceMetadata> {
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let public_url = format!("https://{}", pds_hostname);
|
||||
Json(ProtectedResourceMetadata {
|
||||
resource: public_url.clone(),
|
||||
@@ -72,7 +71,7 @@ pub async fn oauth_protected_resource(
|
||||
pub async fn oauth_authorization_server(
|
||||
State(_state): State<AppState>,
|
||||
) -> Json<AuthorizationServerMetadata> {
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let issuer = format!("https://{}", pds_hostname);
|
||||
Json(AuthorizationServerMetadata {
|
||||
issuer: issuer.clone(),
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::oauth::{
|
||||
verify_client_auth,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::Json;
|
||||
use axum::http::{HeaderMap, Method};
|
||||
use chrono::{Duration, Utc};
|
||||
@@ -101,7 +100,7 @@ pub async fn handle_authorization_code_grant(
|
||||
let dpop_jkt = if let Some(proof) = &dpop_proof {
|
||||
let config = AuthConfig::get();
|
||||
let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
|
||||
let result = verifier.verify_proof(proof, Method::POST.as_str(), &token_endpoint, None)?;
|
||||
if !state
|
||||
@@ -348,7 +347,7 @@ pub async fn handle_refresh_token_grant(
|
||||
let dpop_jkt = if let Some(proof) = &dpop_proof {
|
||||
let config = AuthConfig::get();
|
||||
let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
|
||||
let result = verifier.verify_proof(proof, Method::POST.as_str(), &token_endpoint, None)?;
|
||||
if !state
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::config::AuthConfig;
|
||||
use crate::oauth::OAuthError;
|
||||
use crate::util::pds_hostname;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::Utc;
|
||||
@@ -52,7 +51,7 @@ pub fn create_access_token_with_delegation(
|
||||
) -> Result<String, OAuthError> {
|
||||
use serde_json::json;
|
||||
let jti = uuid::Uuid::new_v4().to_string();
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let issuer = format!("https://{}", pds_hostname);
|
||||
let now = Utc::now().timestamp();
|
||||
let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::helpers::extract_token_claims;
|
||||
use crate::oauth::OAuthError;
|
||||
use crate::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited};
|
||||
use crate::state::AppState;
|
||||
use crate::util::pds_hostname;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{Form, Json};
|
||||
@@ -112,7 +111,7 @@ pub async fn introspect_token(
|
||||
if token_data.expires_at < Utc::now() {
|
||||
return Ok(Json(inactive_response));
|
||||
}
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_hostname = &tranquil_config::get().server.hostname;
|
||||
let issuer = format!("https://{}", pds_hostname);
|
||||
Ok(Json(IntrospectResponse {
|
||||
active: true,
|
||||
|
||||
@@ -124,18 +124,10 @@ impl PlcClient {
|
||||
}
|
||||
|
||||
pub fn with_cache(base_url: Option<String>, cache: Option<Arc<dyn Cache>>) -> Self {
|
||||
let base_url = base_url.unwrap_or_else(|| {
|
||||
std::env::var("PLC_DIRECTORY_URL")
|
||||
.unwrap_or_else(|_| "https://plc.directory".to_string())
|
||||
});
|
||||
let timeout_secs: u64 = std::env::var("PLC_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10);
|
||||
let connect_timeout_secs: u64 = std::env::var("PLC_CONNECT_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5);
|
||||
let cfg = tranquil_config::get();
|
||||
let base_url = base_url.unwrap_or_else(|| cfg.plc.directory_url.clone());
|
||||
let timeout_secs = cfg.plc.timeout_secs;
|
||||
let connect_timeout_secs = cfg.plc.connect_timeout_secs;
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.connect_timeout(Duration::from_secs(connect_timeout_secs))
|
||||
|
||||
@@ -438,12 +438,8 @@ pub async fn start_scheduled_tasks(
|
||||
sso_repo: Arc<dyn SsoRepository>,
|
||||
shutdown: CancellationToken,
|
||||
) {
|
||||
let check_interval = Duration::from_secs(
|
||||
std::env::var("SCHEDULED_DELETE_CHECK_INTERVAL_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(3600),
|
||||
);
|
||||
let check_interval =
|
||||
Duration::from_secs(tranquil_config::get().scheduled.delete_check_interval_secs);
|
||||
|
||||
info!(
|
||||
check_interval_secs = check_interval.as_secs(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::util::pds_hostname;
|
||||
use std::sync::OnceLock;
|
||||
use tranquil_db_traits::SsoProviderType;
|
||||
|
||||
@@ -34,24 +33,58 @@ pub struct SsoConfig {
|
||||
impl SsoConfig {
|
||||
pub fn init() -> &'static Self {
|
||||
SSO_CONFIG.get_or_init(|| {
|
||||
let github = Self::load_provider("GITHUB", false);
|
||||
let discord = Self::load_provider("DISCORD", false);
|
||||
let google = Self::load_provider("GOOGLE", false);
|
||||
let gitlab = Self::load_provider("GITLAB", true);
|
||||
let oidc = Self::load_provider("OIDC", true);
|
||||
let apple = Self::load_apple_provider();
|
||||
|
||||
let config = SsoConfig {
|
||||
github,
|
||||
discord,
|
||||
google,
|
||||
gitlab,
|
||||
oidc,
|
||||
apple,
|
||||
let sso = &tranquil_config::get().sso;
|
||||
let config = SsoConfig {
|
||||
github: Self::provider_from_config(
|
||||
sso.github.enabled,
|
||||
sso.github.client_id.as_deref(),
|
||||
sso.github.client_secret.as_deref(),
|
||||
None,
|
||||
sso.github.display_name.as_deref(),
|
||||
"GITHUB",
|
||||
false,
|
||||
),
|
||||
discord: Self::provider_from_config(
|
||||
sso.discord.enabled,
|
||||
sso.discord.client_id.as_deref(),
|
||||
sso.discord.client_secret.as_deref(),
|
||||
None,
|
||||
sso.discord.display_name.as_deref(),
|
||||
"DISCORD",
|
||||
false,
|
||||
),
|
||||
google: Self::provider_from_config(
|
||||
sso.google.enabled,
|
||||
sso.google.client_id.as_deref(),
|
||||
sso.google.client_secret.as_deref(),
|
||||
None,
|
||||
sso.google.display_name.as_deref(),
|
||||
"GOOGLE",
|
||||
false,
|
||||
),
|
||||
gitlab: Self::provider_from_config(
|
||||
sso.gitlab.enabled,
|
||||
sso.gitlab.client_id.as_deref(),
|
||||
sso.gitlab.client_secret.as_deref(),
|
||||
sso.gitlab.issuer.as_deref(),
|
||||
sso.gitlab.display_name.as_deref(),
|
||||
"GITLAB",
|
||||
true,
|
||||
),
|
||||
oidc: Self::provider_from_config(
|
||||
sso.oidc.enabled,
|
||||
sso.oidc.client_id.as_deref(),
|
||||
sso.oidc.client_secret.as_deref(),
|
||||
sso.oidc.issuer.as_deref(),
|
||||
sso.oidc.display_name.as_deref(),
|
||||
"OIDC",
|
||||
true,
|
||||
),
|
||||
apple: Self::apple_from_config(&sso.apple),
|
||||
};
|
||||
|
||||
if config.is_any_enabled() {
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if hostname.is_empty() || hostname == "localhost" {
|
||||
panic!(
|
||||
"PDS_HOSTNAME must be set to a valid hostname when SSO is enabled. \
|
||||
@@ -72,89 +105,70 @@ impl SsoConfig {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_redirect_uri() -> &'static str {
|
||||
SSO_REDIRECT_URI
|
||||
.get()
|
||||
.map(|s| s.as_str())
|
||||
.expect("SSO redirect URI not initialized - call SsoConfig::init() first")
|
||||
}
|
||||
|
||||
fn load_provider(name: &str, needs_issuer: bool) -> Option<ProviderConfig> {
|
||||
let enabled = crate::util::parse_env_bool(&format!("SSO_{}_ENABLED", name));
|
||||
|
||||
fn provider_from_config(
|
||||
enabled: bool,
|
||||
client_id: Option<&str>,
|
||||
client_secret: Option<&str>,
|
||||
issuer: Option<&str>,
|
||||
display_name: Option<&str>,
|
||||
name: &str,
|
||||
needs_issuer: bool,
|
||||
) -> Option<ProviderConfig> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
let client_id = client_id.filter(|s| !s.is_empty())?;
|
||||
let client_secret = client_secret.filter(|s| !s.is_empty())?;
|
||||
|
||||
let client_id = std::env::var(format!("SSO_{}_CLIENT_ID", name)).ok()?;
|
||||
let client_secret = std::env::var(format!("SSO_{}_CLIENT_SECRET", name)).ok()?;
|
||||
|
||||
if client_id.is_empty() || client_secret.is_empty() {
|
||||
tracing::warn!(
|
||||
"SSO_{} enabled but missing client_id or client_secret",
|
||||
name
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let issuer = if needs_issuer {
|
||||
let issuer_val = std::env::var(format!("SSO_{}_ISSUER", name)).ok();
|
||||
if issuer_val.is_none() || issuer_val.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||
if needs_issuer {
|
||||
let issuer_val = issuer.filter(|s| !s.is_empty());
|
||||
if issuer_val.is_none() {
|
||||
tracing::warn!("SSO_{} requires ISSUER but none provided", name);
|
||||
return None;
|
||||
}
|
||||
issuer_val
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let display_name = std::env::var(format!("SSO_{}_NAME", name)).ok();
|
||||
}
|
||||
|
||||
Some(ProviderConfig {
|
||||
client_id,
|
||||
client_secret,
|
||||
issuer,
|
||||
display_name,
|
||||
client_id: client_id.to_string(),
|
||||
client_secret: client_secret.to_string(),
|
||||
issuer: issuer.map(|s| s.to_string()),
|
||||
display_name: display_name.map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_apple_provider() -> Option<AppleProviderConfig> {
|
||||
let enabled = crate::util::parse_env_bool("SSO_APPLE_ENABLED");
|
||||
|
||||
if !enabled {
|
||||
fn apple_from_config(cfg: &tranquil_config::SsoAppleConfig) -> Option<AppleProviderConfig> {
|
||||
if !cfg.enabled {
|
||||
return None;
|
||||
}
|
||||
let client_id = cfg.client_id.as_deref().filter(|s| !s.is_empty())?;
|
||||
let team_id = cfg.team_id.as_deref().filter(|s| !s.is_empty())?;
|
||||
let key_id = cfg.key_id.as_deref().filter(|s| !s.is_empty())?;
|
||||
let private_key_pem = cfg.private_key.as_deref().filter(|s| !s.is_empty())?;
|
||||
|
||||
let client_id = std::env::var("SSO_APPLE_CLIENT_ID").ok()?;
|
||||
let team_id = std::env::var("SSO_APPLE_TEAM_ID").ok()?;
|
||||
let key_id = std::env::var("SSO_APPLE_KEY_ID").ok()?;
|
||||
let private_key_pem = std::env::var("SSO_APPLE_PRIVATE_KEY").ok()?;
|
||||
|
||||
if client_id.is_empty() {
|
||||
tracing::warn!("SSO_APPLE enabled but missing CLIENT_ID");
|
||||
return None;
|
||||
}
|
||||
if team_id.is_empty() || team_id.len() != 10 {
|
||||
if team_id.len() != 10 {
|
||||
tracing::warn!("SSO_APPLE enabled but TEAM_ID is invalid (must be 10 characters)");
|
||||
return None;
|
||||
}
|
||||
if key_id.is_empty() {
|
||||
tracing::warn!("SSO_APPLE enabled but missing KEY_ID");
|
||||
return None;
|
||||
}
|
||||
if private_key_pem.is_empty() || !private_key_pem.contains("PRIVATE KEY") {
|
||||
if !private_key_pem.contains("PRIVATE KEY") {
|
||||
tracing::warn!("SSO_APPLE enabled but PRIVATE_KEY is invalid");
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AppleProviderConfig {
|
||||
client_id,
|
||||
team_id,
|
||||
key_id,
|
||||
private_key_pem,
|
||||
client_id: client_id.to_string(),
|
||||
team_id: team_id.to_string(),
|
||||
key_id: key_id.to_string(),
|
||||
private_key_pem: private_key_pem.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_redirect_uri() -> &'static str {
|
||||
SSO_REDIRECT_URI
|
||||
.get()
|
||||
.map(|s| s.as_str())
|
||||
.expect("SSO redirect URI not initialized - call SsoConfig::init() first")
|
||||
}
|
||||
|
||||
pub fn get() -> &'static Self {
|
||||
SSO_CONFIG.get_or_init(SsoConfig::default)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::rate_limit::{
|
||||
check_user_rate_limit_with_message,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::util::{pds_hostname, pds_hostname_without_port};
|
||||
|
||||
fn generate_state() -> String {
|
||||
use rand::RngCore;
|
||||
@@ -773,7 +772,7 @@ pub async fn check_handle_available(
|
||||
}
|
||||
};
|
||||
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let full_handle = format!("{}.{}", validated, hostname_for_handles);
|
||||
let handle_typed: crate::types::Handle = match full_handle.parse() {
|
||||
Ok(h) => h,
|
||||
@@ -856,8 +855,8 @@ pub async fn complete_registration(
|
||||
.await?
|
||||
.ok_or(ApiError::SsoSessionExpired)?;
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname_for_handles = pds_hostname_without_port();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
|
||||
let handle = match crate::api::validation::validate_short_handle(&input.handle) {
|
||||
Ok(h) => format!("{}.{}", h, hostname_for_handles),
|
||||
@@ -948,7 +947,7 @@ pub async fn complete_registration(
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else {
|
||||
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return Err(ApiError::InviteCodeRequired);
|
||||
}
|
||||
@@ -1006,8 +1005,11 @@ pub async fn complete_registration(
|
||||
d.to_string()
|
||||
}
|
||||
_ => {
|
||||
let rotation_key = std::env::var("PLC_ROTATION_KEY")
|
||||
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
|
||||
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,
|
||||
@@ -1085,12 +1087,14 @@ pub async fn complete_registration(
|
||||
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
|
||||
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
|
||||
json!({
|
||||
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
|
||||
Some(json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
})
|
||||
});
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let create_input = tranquil_db_traits::CreateSsoAccountInput {
|
||||
handle: handle_typed.clone(),
|
||||
@@ -1299,7 +1303,7 @@ pub async fn complete_registration(
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
|
||||
let hostname = pds_hostname();
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use crate::appview::DidResolver;
|
||||
use crate::auth::webauthn::WebAuthnConfig;
|
||||
use crate::cache::{Cache, DistributedRateLimiter, create_cache};
|
||||
use crate::cache::{create_cache, Cache, DistributedRateLimiter};
|
||||
use crate::circuit_breaker::CircuitBreakers;
|
||||
use crate::config::AuthConfig;
|
||||
use crate::rate_limit::RateLimiters;
|
||||
use crate::repo::PostgresBlockStore;
|
||||
use crate::repo_write_lock::RepoWriteLocks;
|
||||
use crate::sso::{SsoConfig, SsoManager};
|
||||
use crate::storage::{BackupStorage, BlobStorage, create_backup_storage, create_blob_storage};
|
||||
use crate::storage::{create_backup_storage, create_blob_storage, BackupStorage, BlobStorage};
|
||||
use crate::sync::firehose::SequencedEvent;
|
||||
use crate::util::pds_hostname;
|
||||
use sqlx::PgPool;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tranquil_db::{
|
||||
@@ -25,10 +24,10 @@ use tranquil_db::{
|
||||
static RATE_LIMITING_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn init_rate_limit_override() {
|
||||
let disabled = std::env::var("DISABLE_RATE_LIMITING").is_ok();
|
||||
let disabled = tranquil_config::get().server.disable_rate_limiting;
|
||||
RATE_LIMITING_DISABLED.store(disabled, Ordering::Relaxed);
|
||||
if disabled {
|
||||
tracing::warn!("rate limiting is DISABLED via DISABLE_RATE_LIMITING env var");
|
||||
tracing::warn!("rate limiting is DISABLED via configuration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,23 +204,11 @@ impl RateLimitKind {
|
||||
|
||||
impl AppState {
|
||||
pub async fn new(shutdown: CancellationToken) -> Result<Self, Box<dyn Error>> {
|
||||
let database_url = std::env::var("DATABASE_URL")
|
||||
.map_err(|_| "DATABASE_URL environment variable must be set")?;
|
||||
|
||||
let max_connections: u32 = std::env::var("DATABASE_MAX_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(100);
|
||||
|
||||
let min_connections: u32 = std::env::var("DATABASE_MIN_CONNECTIONS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10);
|
||||
|
||||
let acquire_timeout_secs: u64 = std::env::var("DATABASE_ACQUIRE_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10);
|
||||
let cfg = tranquil_config::get();
|
||||
let database_url = &cfg.database.url;
|
||||
let max_connections = cfg.database.max_connections;
|
||||
let min_connections = cfg.database.min_connections;
|
||||
let acquire_timeout_secs = cfg.database.acquire_timeout_secs;
|
||||
|
||||
tracing::info!(
|
||||
"Configuring database pool: max={}, min={}, acquire_timeout={}s",
|
||||
@@ -257,10 +244,7 @@ impl AppState {
|
||||
let blob_store = create_blob_storage().await;
|
||||
let backup_storage = create_backup_storage().await;
|
||||
|
||||
let firehose_buffer_size: usize = std::env::var("FIREHOSE_BUFFER_SIZE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10000);
|
||||
let firehose_buffer_size = tranquil_config::get().firehose.buffer_size;
|
||||
|
||||
let (firehose_tx, _) = broadcast::channel(firehose_buffer_size);
|
||||
let rate_limiters = Arc::new(RateLimiters::new());
|
||||
@@ -271,7 +255,7 @@ impl AppState {
|
||||
let sso_config = SsoConfig::init();
|
||||
let sso_manager = SsoManager::from_config(sso_config);
|
||||
let webauthn_config = Arc::new(
|
||||
WebAuthnConfig::new(pds_hostname())
|
||||
WebAuthnConfig::new(&tranquil_config::get().server.hostname)
|
||||
.expect("Failed to create WebAuthn config at startup"),
|
||||
);
|
||||
|
||||
|
||||
@@ -59,10 +59,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState, params: Subscribe
|
||||
}
|
||||
|
||||
fn get_backfill_hours() -> i64 {
|
||||
std::env::var("FIREHOSE_BACKFILL_HOURS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(72)
|
||||
tranquil_config::get().firehose.backfill_hours
|
||||
}
|
||||
|
||||
async fn handle_socket_inner(
|
||||
@@ -204,10 +201,7 @@ async fn handle_socket_inner(
|
||||
}
|
||||
}
|
||||
}
|
||||
let max_lag_before_disconnect: u64 = std::env::var("FIREHOSE_MAX_LAG")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5000);
|
||||
let max_lag_before_disconnect: u64 = tranquil_config::get().firehose.max_lag;
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = rx.recv() => {
|
||||
|
||||
@@ -145,8 +145,7 @@ impl CarVerifier {
|
||||
}
|
||||
|
||||
async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> {
|
||||
let plc_url = std::env::var("PLC_DIRECTORY_URL")
|
||||
.unwrap_or_else(|_| "https://plc.directory".to_string());
|
||||
let plc_url = tranquil_config::get().plc.directory_url.clone();
|
||||
let url = format!("{}/{}", plc_url, urlencoding::encode(did));
|
||||
let response = self
|
||||
.http_client
|
||||
|
||||
@@ -9,25 +9,12 @@ use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567";
|
||||
const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024;
|
||||
|
||||
static MAX_BLOB_SIZE: OnceLock<usize> = OnceLock::new();
|
||||
static PDS_HOSTNAME: OnceLock<String> = OnceLock::new();
|
||||
static PDS_HOSTNAME_WITHOUT_PORT: OnceLock<String> = OnceLock::new();
|
||||
static DISCORD_BOT_USERNAME: OnceLock<String> = OnceLock::new();
|
||||
static DISCORD_PUBLIC_KEY: OnceLock<ed25519_dalek::VerifyingKey> = OnceLock::new();
|
||||
static DISCORD_APP_ID: OnceLock<String> = OnceLock::new();
|
||||
static TELEGRAM_BOT_USERNAME: OnceLock<String> = OnceLock::new();
|
||||
|
||||
pub fn get_max_blob_size() -> usize {
|
||||
*MAX_BLOB_SIZE.get_or_init(|| {
|
||||
std::env::var("MAX_BLOB_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_BLOB_SIZE)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_token_code() -> String {
|
||||
generate_token_code_parts(2, 5)
|
||||
}
|
||||
@@ -109,18 +96,6 @@ pub fn extract_client_ip(headers: &HeaderMap, addr: Option<SocketAddr>) -> Strin
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
pub fn pds_hostname() -> &'static str {
|
||||
PDS_HOSTNAME
|
||||
.get_or_init(|| std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()))
|
||||
}
|
||||
|
||||
pub fn pds_hostname_without_port() -> &'static str {
|
||||
PDS_HOSTNAME_WITHOUT_PORT.get_or_init(|| {
|
||||
let hostname = pds_hostname();
|
||||
hostname.split(':').next().unwrap_or(hostname).to_string()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_discord_bot_username(username: String) {
|
||||
DISCORD_BOT_USERNAME.set(username).ok();
|
||||
}
|
||||
@@ -154,26 +129,25 @@ pub fn telegram_bot_username() -> Option<&'static str> {
|
||||
}
|
||||
|
||||
pub fn parse_env_bool(key: &str) -> bool {
|
||||
// Check the config system first, then fall back to env var for dynamic
|
||||
// SSO keys that are not in the static config struct.
|
||||
std::env::var(key)
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn pds_public_url() -> String {
|
||||
format!("https://{}", pds_hostname())
|
||||
}
|
||||
|
||||
pub fn build_full_url(path: &str) -> String {
|
||||
let cfg = tranquil_config::get();
|
||||
let normalized_path = if !path.starts_with("/xrpc/")
|
||||
&& (path.starts_with("/com.atproto.")
|
||||
|| path.starts_with("/app.bsky.")
|
||||
|| path.starts_with("/_"))
|
||||
{
|
||||
format!("/xrpc{}", path)
|
||||
format!("/xrpc{path}")
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
format!("{}{}", pds_public_url(), normalized_path)
|
||||
format!("{}{normalized_path}", cfg.server.public_url())
|
||||
}
|
||||
|
||||
pub fn json_to_ipld(value: &JsonValue) -> Ipld {
|
||||
|
||||
@@ -5,6 +5,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-infra = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
|
||||
@@ -15,30 +15,20 @@ pub struct RippleConfig {
|
||||
pub cache_max_bytes: usize,
|
||||
}
|
||||
|
||||
fn parse_env_with_warning<T: std::str::FromStr>(var_name: &str, raw: &str) -> Option<T> {
|
||||
match raw.parse::<T>() {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
var = var_name,
|
||||
value = raw,
|
||||
"invalid env var value, using default"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RippleConfig {
|
||||
pub fn from_env() -> Result<Self, RippleConfigError> {
|
||||
let bind_addr: SocketAddr = std::env::var("RIPPLE_BIND")
|
||||
.unwrap_or_else(|_| "0.0.0.0:0".into())
|
||||
pub fn from_config() -> Result<Self, RippleConfigError> {
|
||||
let ripple = &tranquil_config::get().cache.ripple;
|
||||
|
||||
let bind_addr: SocketAddr = ripple
|
||||
.bind_addr
|
||||
.parse()
|
||||
.map_err(|e| RippleConfigError::InvalidAddr(format!("{e}")))?;
|
||||
|
||||
let seed_peers: Vec<SocketAddr> = std::env::var("RIPPLE_PEERS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
let seed_peers: Vec<SocketAddr> = ripple
|
||||
.peers
|
||||
.as_deref()
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| {
|
||||
s.trim()
|
||||
@@ -47,30 +37,21 @@ impl RippleConfig {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let machine_id: u64 = std::env::var("RIPPLE_MACHINE_ID")
|
||||
.ok()
|
||||
.and_then(|v| parse_env_with_warning::<u64>("RIPPLE_MACHINE_ID", &v))
|
||||
.unwrap_or_else(|| {
|
||||
let host_str = std::fs::read_to_string("/etc/hostname")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| format!("pid-{}", std::process::id()));
|
||||
let input = format!("{host_str}:{bind_addr}:{}", std::process::id());
|
||||
fnv1a(input.as_bytes())
|
||||
});
|
||||
let machine_id = ripple.machine_id.unwrap_or_else(|| {
|
||||
let host_str = std::fs::read_to_string("/etc/hostname")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| format!("pid-{}", std::process::id()));
|
||||
let input = format!("{host_str}:{bind_addr}:{}", std::process::id());
|
||||
fnv1a(input.as_bytes())
|
||||
});
|
||||
|
||||
let gossip_interval_ms: u64 = std::env::var("RIPPLE_GOSSIP_INTERVAL_MS")
|
||||
.ok()
|
||||
.and_then(|v| parse_env_with_warning::<u64>("RIPPLE_GOSSIP_INTERVAL_MS", &v))
|
||||
.unwrap_or(200)
|
||||
.max(50);
|
||||
let gossip_interval_ms = ripple.gossip_interval_ms.max(50);
|
||||
|
||||
let cache_max_mb: usize = std::env::var("RIPPLE_CACHE_MAX_MB")
|
||||
.ok()
|
||||
.and_then(|v| parse_env_with_warning::<usize>("RIPPLE_CACHE_MAX_MB", &v))
|
||||
.unwrap_or(256)
|
||||
.clamp(1, 16_384);
|
||||
|
||||
let cache_max_bytes = cache_max_mb.saturating_mul(1024).saturating_mul(1024);
|
||||
let cache_max_bytes = ripple
|
||||
.cache_max_mb
|
||||
.clamp(1, 16_384)
|
||||
.saturating_mul(1024)
|
||||
.saturating_mul(1024);
|
||||
|
||||
Ok(Self {
|
||||
bind_addr,
|
||||
|
||||
@@ -9,6 +9,7 @@ default = []
|
||||
s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
|
||||
|
||||
[dependencies]
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-infra = { workspace = true }
|
||||
|
||||
async-trait = { workspace = true }
|
||||
|
||||
@@ -121,7 +121,12 @@ mod s3 {
|
||||
|
||||
impl S3BlobStorage {
|
||||
pub async fn new() -> Self {
|
||||
let bucket = std::env::var("S3_BUCKET").expect("S3_BUCKET must be set");
|
||||
let cfg = tranquil_config::get();
|
||||
let bucket = cfg
|
||||
.storage
|
||||
.s3_bucket
|
||||
.clone()
|
||||
.expect("storage.s3_bucket (S3_BUCKET) must be set");
|
||||
let client = create_s3_client().await;
|
||||
Self { client, bucket }
|
||||
}
|
||||
@@ -140,16 +145,20 @@ mod s3 {
|
||||
.load()
|
||||
.await;
|
||||
|
||||
std::env::var("S3_ENDPOINT").ok().map_or_else(
|
||||
|| Client::new(&config),
|
||||
|endpoint| {
|
||||
let s3_config = aws_sdk_s3::config::Builder::from(&config)
|
||||
.endpoint_url(endpoint)
|
||||
.force_path_style(true)
|
||||
.build();
|
||||
Client::from_conf(s3_config)
|
||||
},
|
||||
)
|
||||
tranquil_config::get()
|
||||
.storage
|
||||
.s3_endpoint
|
||||
.as_deref()
|
||||
.map_or_else(
|
||||
|| Client::new(&config),
|
||||
|endpoint| {
|
||||
let s3_config = aws_sdk_s3::config::Builder::from(&config)
|
||||
.endpoint_url(endpoint)
|
||||
.force_path_style(true)
|
||||
.build();
|
||||
Client::from_conf(s3_config)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub struct S3BackupStorage {
|
||||
@@ -159,7 +168,7 @@ mod s3 {
|
||||
|
||||
impl S3BackupStorage {
|
||||
pub async fn new() -> Option<Self> {
|
||||
let bucket = std::env::var("BACKUP_S3_BUCKET").ok()?;
|
||||
let bucket = tranquil_config::get().backup.s3_bucket.clone()?;
|
||||
let client = create_s3_client().await;
|
||||
Some(Self { client, bucket })
|
||||
}
|
||||
@@ -499,12 +508,6 @@ impl FilesystemBlobStorage {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_env() -> Result<Self, StorageError> {
|
||||
let path = std::env::var("BLOB_STORAGE_PATH")
|
||||
.map_err(|_| StorageError::Other("BLOB_STORAGE_PATH not set".into()))?;
|
||||
Self::new(path).await
|
||||
}
|
||||
|
||||
fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> {
|
||||
validate_key(key)?;
|
||||
Ok(split_cid_path(key).map_or_else(
|
||||
@@ -649,12 +652,6 @@ impl FilesystemBackupStorage {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_env() -> Result<Self, StorageError> {
|
||||
let path = std::env::var("BACKUP_STORAGE_PATH")
|
||||
.map_err(|_| StorageError::Other("BACKUP_STORAGE_PATH not set".into()))?;
|
||||
Self::new(path).await
|
||||
}
|
||||
|
||||
fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> {
|
||||
validate_key(key)?;
|
||||
Ok(self.base_path.join(key))
|
||||
@@ -701,7 +698,8 @@ impl BackupStorage for FilesystemBackupStorage {
|
||||
}
|
||||
|
||||
pub async fn create_blob_storage() -> Arc<dyn BlobStorage> {
|
||||
let backend = std::env::var("BLOB_STORAGE_BACKEND").unwrap_or_else(|_| "filesystem".into());
|
||||
let cfg = tranquil_config::get();
|
||||
let backend = &cfg.storage.backend;
|
||||
|
||||
match backend.as_str() {
|
||||
#[cfg(feature = "s3")]
|
||||
@@ -718,7 +716,8 @@ pub async fn create_blob_storage() -> Arc<dyn BlobStorage> {
|
||||
}
|
||||
_ => {
|
||||
tracing::info!("Initializing filesystem blob storage");
|
||||
FilesystemBlobStorage::from_env()
|
||||
let path = cfg.storage.path.clone();
|
||||
FilesystemBlobStorage::new(path)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
@@ -733,16 +732,14 @@ pub async fn create_blob_storage() -> Arc<dyn BlobStorage> {
|
||||
}
|
||||
|
||||
pub async fn create_backup_storage() -> Option<Arc<dyn BackupStorage>> {
|
||||
let enabled = std::env::var("BACKUP_ENABLED")
|
||||
.map(|v| v != "false" && v != "0")
|
||||
.unwrap_or(true);
|
||||
let cfg = tranquil_config::get();
|
||||
|
||||
if !enabled {
|
||||
tracing::info!("Backup storage disabled via BACKUP_ENABLED=false");
|
||||
if !cfg.backup.enabled {
|
||||
tracing::info!("Backup storage disabled via config");
|
||||
return None;
|
||||
}
|
||||
|
||||
let backend = std::env::var("BACKUP_STORAGE_BACKEND").unwrap_or_else(|_| "filesystem".into());
|
||||
let backend = &cfg.backup.backend;
|
||||
|
||||
match backend.as_str() {
|
||||
#[cfg(feature = "s3")]
|
||||
@@ -767,7 +764,9 @@ pub async fn create_backup_storage() -> Option<Arc<dyn BackupStorage>> {
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => FilesystemBackupStorage::from_env().await.map_or_else(
|
||||
_ => {
|
||||
let path = cfg.backup.path.clone();
|
||||
FilesystemBackupStorage::new(path).await.map_or_else(
|
||||
|e| {
|
||||
tracing::error!(
|
||||
"Failed to initialize filesystem backup storage: {}. \
|
||||
@@ -781,7 +780,8 @@ pub async fn create_backup_storage() -> Option<Arc<dyn BackupStorage>> {
|
||||
tracing::info!("Initialized filesystem backup storage");
|
||||
Some(Arc::new(storage) as Arc<dyn BackupStorage>)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}"
|
||||
CRAWLERS: "${CRAWLERS:-https://bsky.network}"
|
||||
volumes:
|
||||
- ./config.toml:/etc/tranquil-pds/config.toml:ro
|
||||
- blob_data:/var/lib/tranquil/blobs
|
||||
- backup_data:/var/lib/tranquil/backups
|
||||
depends_on:
|
||||
|
||||
@@ -11,6 +11,7 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:postgres@db:5432/pds
|
||||
volumes:
|
||||
- ./config.toml:/etc/tranquil-pds/config.toml:ro
|
||||
- blob_data:/var/lib/tranquil/blobs
|
||||
- backup_data:/var/lib/tranquil/backups
|
||||
depends_on:
|
||||
|
||||
+20
-18
@@ -18,10 +18,10 @@ This guide covers deploying Tranquil PDS using containers with podman.
|
||||
If you just want to get running quickly:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
cp example.toml config.toml
|
||||
```
|
||||
|
||||
Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
|
||||
Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
|
||||
|
||||
Build and start:
|
||||
```sh
|
||||
@@ -59,7 +59,7 @@ Run the backend with host networking (so it can access postgres on localhost) an
|
||||
```sh
|
||||
podman run -d --name tranquil-pds \
|
||||
--network=host \
|
||||
--env-file /etc/tranquil-pds/tranquil-pds.env \
|
||||
-v /etc/tranquil-pds/config.toml:/etc/tranquil-pds/config.toml:ro,Z \
|
||||
-v /var/lib/tranquil:/var/lib/tranquil:Z \
|
||||
tranquil-pds:latest
|
||||
```
|
||||
@@ -113,18 +113,22 @@ mkdir -p /etc/containers/systemd
|
||||
mkdir -p /srv/tranquil-pds/{postgres,blobs,backups,certs,acme,config}
|
||||
```
|
||||
|
||||
## Create an environment file
|
||||
## Create a configuration file
|
||||
|
||||
```bash
|
||||
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
|
||||
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
|
||||
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
|
||||
chmod 600 /srv/tranquil-pds/config/config.toml
|
||||
```
|
||||
|
||||
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
> **Note:** Every config option can also be set via environment variables
|
||||
> (see comments in `example.toml`). Environment variables always take
|
||||
> precedence over the config file.
|
||||
|
||||
## Install quadlet definitions
|
||||
|
||||
Copy the quadlet files from the repository:
|
||||
@@ -157,7 +161,6 @@ podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
## Create podman secrets
|
||||
|
||||
```bash
|
||||
source /srv/tranquil-pds/config/tranquil-pds.env
|
||||
echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
|
||||
```
|
||||
|
||||
@@ -264,18 +267,22 @@ podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
```
|
||||
|
||||
## Create an environment file
|
||||
## Create a configuration file
|
||||
|
||||
```sh
|
||||
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
|
||||
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
|
||||
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
|
||||
chmod 600 /srv/tranquil-pds/config/config.toml
|
||||
```
|
||||
|
||||
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
|
||||
```sh
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
> **Note:** Every config option can also be set via environment variables
|
||||
> (see comments in `example.toml`). Environment variables always take
|
||||
> precedence over the config file.
|
||||
|
||||
## Set up compose and nginx
|
||||
|
||||
Copy the production compose and nginx configs:
|
||||
@@ -308,16 +315,11 @@ depend() {
|
||||
after firewall
|
||||
}
|
||||
start_pre() {
|
||||
set -a
|
||||
. /srv/tranquil-pds/config/tranquil-pds.env
|
||||
set +a
|
||||
checkpath -d /srv/tranquil-pds
|
||||
}
|
||||
stop() {
|
||||
ebegin "Stopping ${name}"
|
||||
cd /srv/tranquil-pds
|
||||
set -a
|
||||
. /srv/tranquil-pds/config/tranquil-pds.env
|
||||
set +a
|
||||
podman-compose -f /srv/tranquil-pds/docker-compose.yml down
|
||||
eend $?
|
||||
}
|
||||
|
||||
+14
-5
@@ -73,15 +73,25 @@ cargo build --release
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/tranquil-pds
|
||||
cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env
|
||||
chmod 600 /etc/tranquil-pds/tranquil-pds.env
|
||||
cp /opt/tranquil-pds/example.toml /etc/tranquil-pds/config.toml
|
||||
chmod 600 /etc/tranquil-pds/config.toml
|
||||
```
|
||||
|
||||
Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
Edit `/etc/tranquil-pds/config.toml` and fill in your values. Generate secrets with:
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
> **Note:** Every config option can also be set via environment variables
|
||||
> (see comments in `example.toml`). Environment variables always take
|
||||
> precedence over the config file. You can also pass the config file path
|
||||
> via the `TRANQUIL_PDS_CONFIG` env var instead of `--config`.
|
||||
|
||||
You can validate your configuration before starting the service:
|
||||
```bash
|
||||
/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate
|
||||
```
|
||||
|
||||
## Install frontend files
|
||||
|
||||
```bash
|
||||
@@ -105,8 +115,7 @@ After=network.target postgresql.service
|
||||
Type=simple
|
||||
User=tranquil-pds
|
||||
Group=tranquil-pds
|
||||
EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env
|
||||
ExecStart=/usr/local/bin/tranquil-pds
|
||||
ExecStart=/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
ProtectSystem=strict
|
||||
|
||||
@@ -9,13 +9,16 @@ If you're reaching for kubernetes for this app, you're experienced enough to kno
|
||||
You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com`. User handles are served as subdomains.
|
||||
|
||||
The container image expects:
|
||||
- A TOML config file mounted at `/etc/tranquil-pds/config.toml` (or passed via `--config`)
|
||||
- `DATABASE_URL` - postgres connection string
|
||||
- `BLOB_STORAGE_PATH` - path to blob storage (mount a PV here)
|
||||
- `BACKUP_STORAGE_PATH` - path for repo backups (optional but recommended)
|
||||
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
|
||||
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
|
||||
- `CRAWLERS` - typically `https://bsky.network`
|
||||
and more, check the .env.example.
|
||||
|
||||
and more, check the example.toml for all options. Environment variables can override any TOML value.
|
||||
You can also point to a config file via the `TRANQUIL_PDS_CONFIG` env var.
|
||||
|
||||
Health check: `GET /xrpc/_health`
|
||||
|
||||
|
||||
+509
@@ -0,0 +1,509 @@
|
||||
[server]
|
||||
# Public hostname of the PDS (e.g. `pds.example.com`).
|
||||
#
|
||||
# Can also be specified via environment variable `PDS_HOSTNAME`.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#hostname =
|
||||
|
||||
# Address to bind the HTTP server to.
|
||||
#
|
||||
# Can also be specified via environment variable `SERVER_HOST`.
|
||||
#
|
||||
# Default value: "127.0.0.1"
|
||||
#host = "127.0.0.1"
|
||||
|
||||
# Port to bind the HTTP server to.
|
||||
#
|
||||
# Can also be specified via environment variable `SERVER_PORT`.
|
||||
#
|
||||
# Default value: 3000
|
||||
#port = 3000
|
||||
|
||||
# List of domains for user handles.
|
||||
# Defaults to the PDS hostname when not set.
|
||||
#
|
||||
# Can also be specified via environment variable `PDS_USER_HANDLE_DOMAINS`.
|
||||
#user_handle_domains =
|
||||
|
||||
# List of domains available for user registration.
|
||||
# Defaults to the PDS hostname when not set.
|
||||
#
|
||||
# Can also be specified via environment variable `AVAILABLE_USER_DOMAINS`.
|
||||
#available_user_domains =
|
||||
|
||||
# Enable PDS-hosted did:web identities. Hosting did:web requires a
|
||||
# long-term commitment to serve DID documents; opt-in only.
|
||||
#
|
||||
# Can also be specified via environment variable `ENABLE_PDS_HOSTED_DID_WEB`.
|
||||
#
|
||||
# Default value: false
|
||||
#enable_pds_hosted_did_web = false
|
||||
|
||||
# When set to true, skip age-assurance birthday prompt for all accounts.
|
||||
#
|
||||
# Can also be specified via environment variable `PDS_AGE_ASSURANCE_OVERRIDE`.
|
||||
#
|
||||
# Default value: false
|
||||
#age_assurance_override = false
|
||||
|
||||
# Require an invite code for new account registration.
|
||||
#
|
||||
# Can also be specified via environment variable `INVITE_CODE_REQUIRED`.
|
||||
#
|
||||
# Default value: true
|
||||
#invite_code_required = true
|
||||
|
||||
# Allow HTTP (non-TLS) proxy requests. Only useful during development.
|
||||
#
|
||||
# Can also be specified via environment variable `ALLOW_HTTP_PROXY`.
|
||||
#
|
||||
# Default value: false
|
||||
#allow_http_proxy = false
|
||||
|
||||
# Disable all rate limiting. Should only be used in testing.
|
||||
#
|
||||
# Can also be specified via environment variable `DISABLE_RATE_LIMITING`.
|
||||
#
|
||||
# Default value: false
|
||||
#disable_rate_limiting = false
|
||||
|
||||
# List of additional banned words for handle validation.
|
||||
#
|
||||
# Can also be specified via environment variable `PDS_BANNED_WORDS`.
|
||||
#banned_words =
|
||||
|
||||
# URL to a privacy policy page.
|
||||
#
|
||||
# Can also be specified via environment variable `PRIVACY_POLICY_URL`.
|
||||
#privacy_policy_url =
|
||||
|
||||
# URL to terms of service page.
|
||||
#
|
||||
# Can also be specified via environment variable `TERMS_OF_SERVICE_URL`.
|
||||
#terms_of_service_url =
|
||||
|
||||
# Operator contact email address.
|
||||
#
|
||||
# Can also be specified via environment variable `CONTACT_EMAIL`.
|
||||
#contact_email =
|
||||
|
||||
# Maximum allowed blob size in bytes (default 10 GiB).
|
||||
#
|
||||
# Can also be specified via environment variable `MAX_BLOB_SIZE`.
|
||||
#
|
||||
# Default value: 10737418240
|
||||
#max_blob_size = 10737418240
|
||||
|
||||
[database]
|
||||
# PostgreSQL connection URL.
|
||||
#
|
||||
# Can also be specified via environment variable `DATABASE_URL`.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#url =
|
||||
|
||||
# Maximum number of connections in the pool.
|
||||
#
|
||||
# Can also be specified via environment variable `DATABASE_MAX_CONNECTIONS`.
|
||||
#
|
||||
# Default value: 100
|
||||
#max_connections = 100
|
||||
|
||||
# Minimum number of idle connections kept in the pool.
|
||||
#
|
||||
# Can also be specified via environment variable `DATABASE_MIN_CONNECTIONS`.
|
||||
#
|
||||
# Default value: 10
|
||||
#min_connections = 10
|
||||
|
||||
# Timeout in seconds when acquiring a connection from the pool.
|
||||
#
|
||||
# Can also be specified via environment variable `DATABASE_ACQUIRE_TIMEOUT_SECS`.
|
||||
#
|
||||
# Default value: 10
|
||||
#acquire_timeout_secs = 10
|
||||
|
||||
[secrets]
|
||||
# Secret used for signing JWTs. Must be at least 32 characters in
|
||||
# production.
|
||||
#
|
||||
# Can also be specified via environment variable `JWT_SECRET`.
|
||||
#jwt_secret =
|
||||
|
||||
# Secret used for DPoP proof validation. Must be at least 32 characters
|
||||
# in production.
|
||||
#
|
||||
# Can also be specified via environment variable `DPOP_SECRET`.
|
||||
#dpop_secret =
|
||||
|
||||
# Master key used for key-encryption and HKDF derivation. Must be at
|
||||
# least 32 characters in production.
|
||||
#
|
||||
# Can also be specified via environment variable `MASTER_KEY`.
|
||||
#master_key =
|
||||
|
||||
# PLC rotation key (DID key). If not set, user-level keys are used.
|
||||
#
|
||||
# Can also be specified via environment variable `PLC_ROTATION_KEY`.
|
||||
#plc_rotation_key =
|
||||
|
||||
# Allow insecure/test secrets. NEVER enable in production.
|
||||
#
|
||||
# Can also be specified via environment variable `TRANQUIL_PDS_ALLOW_INSECURE_SECRETS`.
|
||||
#
|
||||
# Default value: false
|
||||
#allow_insecure = false
|
||||
|
||||
[storage]
|
||||
# Storage backend: `filesystem` or `s3`.
|
||||
#
|
||||
# Can also be specified via environment variable `BLOB_STORAGE_BACKEND`.
|
||||
#
|
||||
# Default value: "filesystem"
|
||||
#backend = "filesystem"
|
||||
|
||||
# Path on disk for the filesystem blob backend.
|
||||
#
|
||||
# Can also be specified via environment variable `BLOB_STORAGE_PATH`.
|
||||
#
|
||||
# Default value: "/var/lib/tranquil-pds/blobs"
|
||||
#path = "/var/lib/tranquil-pds/blobs"
|
||||
|
||||
# S3 bucket name for blob storage.
|
||||
#
|
||||
# Can also be specified via environment variable `S3_BUCKET`.
|
||||
#s3_bucket =
|
||||
|
||||
# Custom S3 endpoint URL (for MinIO, R2, etc.).
|
||||
#
|
||||
# Can also be specified via environment variable `S3_ENDPOINT`.
|
||||
#s3_endpoint =
|
||||
|
||||
[backup]
|
||||
# Enable automatic backups.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_ENABLED`.
|
||||
#
|
||||
# Default value: true
|
||||
#enabled = true
|
||||
|
||||
# Backup storage backend: `filesystem` or `s3`.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_STORAGE_BACKEND`.
|
||||
#
|
||||
# Default value: "filesystem"
|
||||
#backend = "filesystem"
|
||||
|
||||
# Path on disk for the filesystem backup backend.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_STORAGE_PATH`.
|
||||
#
|
||||
# Default value: "/var/lib/tranquil-pds/backups"
|
||||
#path = "/var/lib/tranquil-pds/backups"
|
||||
|
||||
# S3 bucket name for backups.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_S3_BUCKET`.
|
||||
#s3_bucket =
|
||||
|
||||
# Number of backup revisions to keep per account.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_RETENTION_COUNT`.
|
||||
#
|
||||
# Default value: 7
|
||||
#retention_count = 7
|
||||
|
||||
# Seconds between backup runs.
|
||||
#
|
||||
# Can also be specified via environment variable `BACKUP_INTERVAL_SECS`.
|
||||
#
|
||||
# Default value: 86400
|
||||
#interval_secs = 86400
|
||||
|
||||
[cache]
|
||||
# Cache backend: `ripple` (default, built-in gossip) or `valkey`.
|
||||
#
|
||||
# Can also be specified via environment variable `CACHE_BACKEND`.
|
||||
#
|
||||
# Default value: "ripple"
|
||||
#backend = "ripple"
|
||||
|
||||
# Valkey / Redis connection URL. Required when `backend = "valkey"`.
|
||||
#
|
||||
# Can also be specified via environment variable `VALKEY_URL`.
|
||||
#valkey_url =
|
||||
|
||||
[cache.ripple]
|
||||
# Address to bind the Ripple gossip protocol listener.
|
||||
#
|
||||
# Can also be specified via environment variable `RIPPLE_BIND`.
|
||||
#
|
||||
# Default value: "0.0.0.0:0"
|
||||
#bind_addr = "0.0.0.0:0"
|
||||
|
||||
# List of seed peer addresses.
|
||||
#
|
||||
# Can also be specified via environment variable `RIPPLE_PEERS`.
|
||||
#peers =
|
||||
|
||||
# Unique machine identifier. Auto-derived from hostname when not set.
|
||||
#
|
||||
# Can also be specified via environment variable `RIPPLE_MACHINE_ID`.
|
||||
#machine_id =
|
||||
|
||||
# Gossip protocol interval in milliseconds.
|
||||
#
|
||||
# Can also be specified via environment variable `RIPPLE_GOSSIP_INTERVAL_MS`.
|
||||
#
|
||||
# Default value: 200
|
||||
#gossip_interval_ms = 200
|
||||
|
||||
# Maximum cache size in megabytes.
|
||||
#
|
||||
# Can also be specified via environment variable `RIPPLE_CACHE_MAX_MB`.
|
||||
#
|
||||
# Default value: 256
|
||||
#cache_max_mb = 256
|
||||
|
||||
[plc]
|
||||
# Base URL of the PLC directory.
|
||||
#
|
||||
# Can also be specified via environment variable `PLC_DIRECTORY_URL`.
|
||||
#
|
||||
# Default value: "https://plc.directory"
|
||||
#directory_url = "https://plc.directory"
|
||||
|
||||
# HTTP request timeout in seconds.
|
||||
#
|
||||
# Can also be specified via environment variable `PLC_TIMEOUT_SECS`.
|
||||
#
|
||||
# Default value: 10
|
||||
#timeout_secs = 10
|
||||
|
||||
# TCP connect timeout in seconds.
|
||||
#
|
||||
# Can also be specified via environment variable `PLC_CONNECT_TIMEOUT_SECS`.
|
||||
#
|
||||
# Default value: 5
|
||||
#connect_timeout_secs = 5
|
||||
|
||||
# Seconds to cache DID documents in memory.
|
||||
#
|
||||
# Can also be specified via environment variable `DID_CACHE_TTL_SECS`.
|
||||
#
|
||||
# Default value: 300
|
||||
#did_cache_ttl_secs = 300
|
||||
|
||||
[firehose]
|
||||
# Size of the in-memory broadcast buffer for firehose events.
|
||||
#
|
||||
# Can also be specified via environment variable `FIREHOSE_BUFFER_SIZE`.
|
||||
#
|
||||
# Default value: 10000
|
||||
#buffer_size = 10000
|
||||
|
||||
# How many hours of historical events to replay for cursor-based
|
||||
# firehose connections.
|
||||
#
|
||||
# Can also be specified via environment variable `FIREHOSE_BACKFILL_HOURS`.
|
||||
#
|
||||
# Default value: 72
|
||||
#backfill_hours = 72
|
||||
|
||||
# Maximum number of lagged events before disconnecting a slow consumer.
|
||||
#
|
||||
# Can also be specified via environment variable `FIREHOSE_MAX_LAG`.
|
||||
#
|
||||
# Default value: 5000
|
||||
#max_lag = 5000
|
||||
|
||||
# List of relay / crawler notification URLs.
|
||||
#
|
||||
# Can also be specified via environment variable `CRAWLERS`.
|
||||
#crawlers =
|
||||
|
||||
[email]
|
||||
# Sender email address. When unset, email sending is disabled.
|
||||
#
|
||||
# Can also be specified via environment variable `MAIL_FROM_ADDRESS`.
|
||||
#from_address =
|
||||
|
||||
# Display name used in the `From` header.
|
||||
#
|
||||
# Can also be specified via environment variable `MAIL_FROM_NAME`.
|
||||
#
|
||||
# Default value: "Tranquil PDS"
|
||||
#from_name = "Tranquil PDS"
|
||||
|
||||
# Path to the `sendmail` binary.
|
||||
#
|
||||
# Can also be specified via environment variable `SENDMAIL_PATH`.
|
||||
#
|
||||
# Default value: "/usr/sbin/sendmail"
|
||||
#sendmail_path = "/usr/sbin/sendmail"
|
||||
|
||||
[discord]
|
||||
# Discord bot token. When unset, Discord integration is disabled.
|
||||
#
|
||||
# Can also be specified via environment variable `DISCORD_BOT_TOKEN`.
|
||||
#bot_token =
|
||||
|
||||
[telegram]
|
||||
# Telegram bot token. When unset, Telegram integration is disabled.
|
||||
#
|
||||
# Can also be specified via environment variable `TELEGRAM_BOT_TOKEN`.
|
||||
#bot_token =
|
||||
|
||||
# Secret token for incoming webhook verification.
|
||||
#
|
||||
# Can also be specified via environment variable `TELEGRAM_WEBHOOK_SECRET`.
|
||||
#webhook_secret =
|
||||
|
||||
[signal]
|
||||
# Path to the `signal-cli` binary.
|
||||
#
|
||||
# Can also be specified via environment variable `SIGNAL_CLI_PATH`.
|
||||
#
|
||||
# Default value: "/usr/local/bin/signal-cli"
|
||||
#cli_path = "/usr/local/bin/signal-cli"
|
||||
|
||||
# Sender phone number. When unset, Signal integration is disabled.
|
||||
#
|
||||
# Can also be specified via environment variable `SIGNAL_SENDER_NUMBER`.
|
||||
#sender_number =
|
||||
|
||||
[notifications]
|
||||
# Polling interval in milliseconds for the comms queue.
|
||||
#
|
||||
# Can also be specified via environment variable `NOTIFICATION_POLL_INTERVAL_MS`.
|
||||
#
|
||||
# Default value: 1000
|
||||
#poll_interval_ms = 1000
|
||||
|
||||
# Number of notifications to process per batch.
|
||||
#
|
||||
# Can also be specified via environment variable `NOTIFICATION_BATCH_SIZE`.
|
||||
#
|
||||
# Default value: 100
|
||||
#batch_size = 100
|
||||
|
||||
[sso]
|
||||
[sso.github]
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
#client_id =
|
||||
|
||||
#client_secret =
|
||||
|
||||
#display_name =
|
||||
|
||||
[sso.discord]
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
#client_id =
|
||||
|
||||
#client_secret =
|
||||
|
||||
#display_name =
|
||||
|
||||
[sso.google]
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
#client_id =
|
||||
|
||||
#client_secret =
|
||||
|
||||
#display_name =
|
||||
|
||||
[sso.gitlab]
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
#client_id =
|
||||
|
||||
#client_secret =
|
||||
|
||||
#issuer =
|
||||
|
||||
#display_name =
|
||||
|
||||
[sso.oidc]
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
#client_id =
|
||||
|
||||
#client_secret =
|
||||
|
||||
#issuer =
|
||||
|
||||
#display_name =
|
||||
|
||||
[sso.apple]
|
||||
# Can also be specified via environment variable `SSO_APPLE_ENABLED`.
|
||||
# Default value: false
|
||||
#enabled = false
|
||||
|
||||
# Can also be specified via environment variable `SSO_APPLE_CLIENT_ID`.
|
||||
#client_id =
|
||||
|
||||
# Can also be specified via environment variable `SSO_APPLE_TEAM_ID`.
|
||||
#team_id =
|
||||
|
||||
# Can also be specified via environment variable `SSO_APPLE_KEY_ID`.
|
||||
#key_id =
|
||||
|
||||
# Can also be specified via environment variable `SSO_APPLE_PRIVATE_KEY`.
|
||||
#private_key =
|
||||
|
||||
[moderation]
|
||||
# External report-handling service URL.
|
||||
#
|
||||
# Can also be specified via environment variable `REPORT_SERVICE_URL`.
|
||||
#report_service_url =
|
||||
|
||||
# DID of the external report-handling service.
|
||||
#
|
||||
# Can also be specified via environment variable `REPORT_SERVICE_DID`.
|
||||
#report_service_did =
|
||||
|
||||
[import]
|
||||
# Whether the PDS accepts repo imports.
|
||||
#
|
||||
# Can also be specified via environment variable `ACCEPTING_REPO_IMPORTS`.
|
||||
#
|
||||
# Default value: true
|
||||
#accepting = true
|
||||
|
||||
# Maximum allowed import archive size in bytes (default 1 GiB).
|
||||
#
|
||||
# Can also be specified via environment variable `MAX_IMPORT_SIZE`.
|
||||
#
|
||||
# Default value: 1073741824
|
||||
#max_size = 1073741824
|
||||
|
||||
# Maximum number of blocks allowed in an import.
|
||||
#
|
||||
# Can also be specified via environment variable `MAX_IMPORT_BLOCKS`.
|
||||
#
|
||||
# Default value: 500000
|
||||
#max_blocks = 500000
|
||||
|
||||
# Skip CAR verification during import. Only for development/debugging.
|
||||
#
|
||||
# Can also be specified via environment variable `SKIP_IMPORT_VERIFICATION`.
|
||||
#
|
||||
# Default value: false
|
||||
#skip_verification = false
|
||||
|
||||
[scheduled]
|
||||
# Interval in seconds between scheduled delete checks.
|
||||
#
|
||||
# Can also be specified via environment variable `SCHEDULED_DELETE_CHECK_INTERVAL_SECS`.
|
||||
#
|
||||
# Default value: 3600
|
||||
#delete_check_interval_secs = 3600
|
||||
Reference in New Issue
Block a user