Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

refactor: toml config #24

merged opened by isabelroses.com targeting main

most certainly a improvement but almost certainly going to have some errors

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:qxichs7jsycphrsmbujwqbfb/sh.tangled.repo.pull/3meorfx6aki22
+538 -336
Interdiff #0 โ†’ #1
Cargo.lock

This file has not been changed.

Cargo.toml

This file has not been changed.

crates/tranquil-auth/Cargo.toml

This file has not been changed.

crates/tranquil-auth/src/token.rs

This file has not been changed.

crates/tranquil-cache/Cargo.toml

This file has not been changed.

+19 -13
crates/tranquil-cache/src/lib.rs
··· 172 pub async fn create_cache( 173 shutdown: tokio_util::sync::CancellationToken, 174 ) -> (Arc<dyn Cache>, Arc<dyn DistributedRateLimiter>) { 175 - let valkey_url = tranquil_config::try_get().and_then(|c| c.cache.valkey_url.clone()); 176 177 #[cfg(feature = "valkey")] 178 - if let Some(url) = valkey_url.as_deref() { 179 - match ValkeyCache::new(url).await { 180 - Ok(cache) => { 181 - tracing::info!("using valkey cache at {url}"); 182 - let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection())); 183 - return (Arc::new(cache), rate_limiter); 184 } 185 - Err(e) => { 186 - tracing::warn!("failed to connect to valkey: {e}. falling back to ripple."); 187 - } 188 } 189 } 190 191 #[cfg(not(feature = "valkey"))] 192 - if valkey_url.is_some() { 193 tracing::warn!( 194 - "valkey_url is set but binary was compiled without valkey feature. using ripple." 195 ); 196 } 197 198 - match tranquil_ripple::RippleConfig::from_env() { 199 Ok(config) => { 200 let peer_count = config.seed_peers.len(); 201 match tranquil_ripple::RippleEngine::start(config, shutdown).await {
··· 172 pub async fn create_cache( 173 shutdown: tokio_util::sync::CancellationToken, 174 ) -> (Arc<dyn Cache>, Arc<dyn DistributedRateLimiter>) { 175 + let cache_cfg = tranquil_config::try_get().map(|c| &c.cache); 176 + let backend = cache_cfg.map(|c| c.backend.as_str()).unwrap_or("ripple"); 177 + let valkey_url = cache_cfg.and_then(|c| c.valkey_url.as_deref()); 178 179 #[cfg(feature = "valkey")] 180 + if backend == "valkey" { 181 + if let Some(url) = valkey_url { 182 + match ValkeyCache::new(url).await { 183 + Ok(cache) => { 184 + tracing::info!("using valkey cache at {url}"); 185 + let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection())); 186 + return (Arc::new(cache), rate_limiter); 187 + } 188 + Err(e) => { 189 + tracing::warn!("failed to connect to valkey: {e}. falling back to ripple."); 190 + } 191 } 192 + } else { 193 + tracing::warn!("cache.backend is \"valkey\" but VALKEY_URL is not set. using ripple."); 194 } 195 } 196 197 #[cfg(not(feature = "valkey"))] 198 + if backend == "valkey" { 199 tracing::warn!( 200 + "cache.backend is \"valkey\" but binary was compiled without valkey feature. using ripple." 201 ); 202 } 203 204 + match tranquil_ripple::RippleConfig::from_config() { 205 Ok(config) => { 206 let peer_count = config.seed_peers.len(); 207 match tranquil_ripple::RippleEngine::start(config, shutdown).await {
crates/tranquil-comms/Cargo.toml

This file has not been changed.

+9 -13
crates/tranquil-comms/src/sender.rs
··· 112 } 113 114 impl EmailSender { 115 - pub fn new(from_address: String, from_name: String) -> Self { 116 - let sendmail_path = tranquil_config::try_get() 117 - .map(|c| c.email.sendmail_path.clone()) 118 - .unwrap_or_else(|| "/usr/sbin/sendmail".to_string()); 119 Self { 120 from_address, 121 from_name, ··· 123 } 124 } 125 126 - pub fn from_env() -> Option<Self> { 127 - let cfg = tranquil_config::try_get()?; 128 let from_address = cfg.email.from_address.clone()?; 129 let from_name = cfg.email.from_name.clone(); 130 - Some(Self::new(from_address, from_name)) 131 } 132 133 pub fn format_email(&self, notification: &QueuedComms) -> String { ··· 192 } 193 } 194 195 - pub fn from_env() -> Option<Self> { 196 - let bot_token = tranquil_config::try_get()?.discord.bot_token.clone()?; 197 Some(Self::new(bot_token)) 198 } 199 ··· 456 } 457 } 458 459 - pub fn from_env() -> Option<Self> { 460 - let bot_token = tranquil_config::try_get()?.telegram.bot_token.clone()?; 461 Some(Self::new(bot_token)) 462 } 463 ··· 588 } 589 } 590 591 - pub fn from_env() -> Option<Self> { 592 - let cfg = tranquil_config::try_get()?; 593 let signal_cli_path = cfg.signal.cli_path.clone(); 594 let sender_number = cfg.signal.sender_number.clone()?; 595 Some(Self::new(signal_cli_path, sender_number))
··· 112 } 113 114 impl EmailSender { 115 + pub fn new(from_address: String, from_name: String, sendmail_path: String) -> Self { 116 Self { 117 from_address, 118 from_name, ··· 120 } 121 } 122 123 + pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> { 124 let from_address = cfg.email.from_address.clone()?; 125 let from_name = cfg.email.from_name.clone(); 126 + let sendmail_path = cfg.email.sendmail_path.clone(); 127 + Some(Self::new(from_address, from_name, sendmail_path)) 128 } 129 130 pub fn format_email(&self, notification: &QueuedComms) -> String { ··· 189 } 190 } 191 192 + pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> { 193 + let bot_token = cfg.discord.bot_token.clone()?; 194 Some(Self::new(bot_token)) 195 } 196 ··· 453 } 454 } 455 456 + pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> { 457 + let bot_token = cfg.telegram.bot_token.clone()?; 458 Some(Self::new(bot_token)) 459 } 460 ··· 585 } 586 } 587 588 + pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> { 589 let signal_cli_path = cfg.signal.cli_path.clone(); 590 let sender_number = cfg.signal.sender_number.clone()?; 591 Some(Self::new(signal_cli_path, sender_number))
crates/tranquil-config/Cargo.toml

This file has not been changed.

+58 -117
crates/tranquil-config/src/lib.rs
··· 113 pub import: ImportConfig, 114 115 #[config(nested)] 116 - pub crawlers: CrawlersConfig, 117 - 118 - #[config(nested)] 119 - pub ripple: RippleConfig, 120 - 121 - #[config(nested)] 122 pub scheduled: ScheduledConfig, 123 } 124 ··· 264 ); 265 } 266 267 if errors.is_empty() { 268 Ok(()) 269 } else { ··· 372 #[config(env = "SERVER_PORT", default = 3000)] 373 pub port: u16, 374 375 - /// Comma-separated list of domains that are considered service handle 376 - /// domains. Defaults to the PDS hostname when not set. 377 - #[config(env = "PDS_SERVICE_HANDLE_DOMAINS", parse_env = split_comma_list)] 378 - pub service_handle_domains: Option<Vec<String>>, 379 380 - /// Comma-separated list of domains available for user registration. 381 /// Defaults to the PDS hostname when not set. 382 #[config(env = "AVAILABLE_USER_DOMAINS", parse_env = split_comma_list)] 383 pub available_user_domains: Option<Vec<String>>, 384 385 - /// Enable self-hosted did:web identities. 386 - #[config(env = "ENABLE_SELF_HOSTED_DID_WEB", default = true)] 387 - pub enable_self_hosted_did_web: bool, 388 389 /// When set to true, skip age-assurance birthday prompt for all accounts. 390 #[config(env = "PDS_AGE_ASSURANCE_OVERRIDE", default = false)] 391 pub age_assurance_override: bool, 392 393 /// Require an invite code for new account registration. 394 - #[config(env = "INVITE_CODE_REQUIRED", default = false)] 395 pub invite_code_required: bool, 396 397 /// Allow HTTP (non-TLS) proxy requests. Only useful during development. ··· 402 #[config(env = "DISABLE_RATE_LIMITING", default = false)] 403 pub disable_rate_limiting: bool, 404 405 - /// Comma-separated list of additional banned words for handle validation. 406 #[config(env = "PDS_BANNED_WORDS", parse_env = split_comma_list)] 407 pub banned_words: Option<Vec<String>>, 408 ··· 447 .unwrap_or_else(|| vec![self.hostname_without_port().to_string()]) 448 } 449 450 - /// Returns the service handle domains, falling back to `[hostname_without_port]`. 451 - pub fn service_handle_domain_list(&self) -> Vec<String> { 452 - self.service_handle_domains 453 .clone() 454 .unwrap_or_else(|| vec![self.hostname_without_port().to_string()]) 455 } 456 } 457 458 - // --------------------------------------------------------------------------- 459 - // Database 460 - // --------------------------------------------------------------------------- 461 - 462 #[derive(Debug, Config)] 463 pub struct DatabaseConfig { 464 /// PostgreSQL connection URL. ··· 478 pub acquire_timeout_secs: u64, 479 } 480 481 - // --------------------------------------------------------------------------- 482 - // Secrets 483 - // --------------------------------------------------------------------------- 484 - 485 #[derive(Config)] 486 pub struct SecretsConfig { 487 /// Secret used for signing JWTs. Must be at least 32 characters in ··· 579 } 580 } 581 582 - // --------------------------------------------------------------------------- 583 - // Storage (blob) 584 - // --------------------------------------------------------------------------- 585 - 586 #[derive(Debug, Config)] 587 pub struct StorageConfig { 588 /// Storage backend: `filesystem` or `s3`. ··· 602 pub s3_endpoint: Option<String>, 603 } 604 605 - // --------------------------------------------------------------------------- 606 - // Backup 607 - // --------------------------------------------------------------------------- 608 - 609 #[derive(Debug, Config)] 610 pub struct BackupConfig { 611 /// Enable automatic backups. ··· 633 pub interval_secs: u64, 634 } 635 636 - // --------------------------------------------------------------------------- 637 - // Cache 638 - // --------------------------------------------------------------------------- 639 - 640 #[derive(Debug, Config)] 641 pub struct CacheConfig { 642 - /// Valkey / Redis connection URL. When set, Valkey is used for caching and 643 - /// distributed rate-limiting instead of the built-in Ripple protocol. 644 #[config(env = "VALKEY_URL")] 645 pub valkey_url: Option<String>, 646 } 647 648 - // --------------------------------------------------------------------------- 649 - // PLC directory 650 - // --------------------------------------------------------------------------- 651 - 652 #[derive(Debug, Config)] 653 pub struct PlcConfig { 654 /// Base URL of the PLC directory. ··· 668 pub did_cache_ttl_secs: u64, 669 } 670 671 - // --------------------------------------------------------------------------- 672 - // Firehose 673 - // --------------------------------------------------------------------------- 674 - 675 #[derive(Debug, Config)] 676 pub struct FirehoseConfig { 677 /// Size of the in-memory broadcast buffer for firehose events. ··· 686 /// Maximum number of lagged events before disconnecting a slow consumer. 687 #[config(env = "FIREHOSE_MAX_LAG", default = 5000)] 688 pub max_lag: u64, 689 } 690 691 - // --------------------------------------------------------------------------- 692 - // Email 693 - // --------------------------------------------------------------------------- 694 695 #[derive(Debug, Config)] 696 pub struct EmailConfig { ··· 707 pub sendmail_path: String, 708 } 709 710 - // --------------------------------------------------------------------------- 711 - // Discord 712 - // --------------------------------------------------------------------------- 713 - 714 #[derive(Debug, Config)] 715 pub struct DiscordConfig { 716 /// Discord bot token. When unset, Discord integration is disabled. ··· 718 pub bot_token: Option<String>, 719 } 720 721 - // --------------------------------------------------------------------------- 722 - // Telegram 723 - // --------------------------------------------------------------------------- 724 - 725 #[derive(Debug, Config)] 726 pub struct TelegramConfig { 727 /// Telegram bot token. When unset, Telegram integration is disabled. ··· 733 pub webhook_secret: Option<String>, 734 } 735 736 - // --------------------------------------------------------------------------- 737 - // Signal 738 - // --------------------------------------------------------------------------- 739 - 740 #[derive(Debug, Config)] 741 pub struct SignalConfig { 742 /// Path to the `signal-cli` binary. ··· 748 pub sender_number: Option<String>, 749 } 750 751 - // --------------------------------------------------------------------------- 752 - // Notification service 753 - // --------------------------------------------------------------------------- 754 - 755 #[derive(Debug, Config)] 756 pub struct NotificationConfig { 757 /// Polling interval in milliseconds for the comms queue. ··· 763 pub batch_size: i64, 764 } 765 766 - // --------------------------------------------------------------------------- 767 - // SSO 768 - // --------------------------------------------------------------------------- 769 - 770 #[derive(Debug, Config)] 771 pub struct SsoConfig { 772 #[config(nested)] ··· 838 pub private_key: Option<String>, 839 } 840 841 - // --------------------------------------------------------------------------- 842 - // Moderation 843 - // --------------------------------------------------------------------------- 844 - 845 #[derive(Debug, Config)] 846 pub struct ModerationConfig { 847 /// External report-handling service URL. ··· 853 pub report_service_did: Option<String>, 854 } 855 856 - // --------------------------------------------------------------------------- 857 - // Repo import 858 - // --------------------------------------------------------------------------- 859 - 860 #[derive(Debug, Config)] 861 pub struct ImportConfig { 862 /// Whether the PDS accepts repo imports. ··· 876 pub skip_verification: bool, 877 } 878 879 - // --------------------------------------------------------------------------- 880 - // Crawlers 881 - // --------------------------------------------------------------------------- 882 - 883 - #[derive(Debug, Config)] 884 - pub struct CrawlersConfig { 885 - /// Comma-separated list of relay / crawler notification URLs. 886 - #[config(env = "CRAWLERS", parse_env = split_comma_list)] 887 - pub urls: Option<Vec<String>>, 888 - } 889 - 890 - impl CrawlersConfig { 891 - /// Returns the list of crawler URLs, falling back to `["https://bsky.network"]` 892 - /// when none are configured. 893 - pub fn url_list(&self) -> Vec<String> { 894 - self.urls 895 - .clone() 896 - .unwrap_or_else(|| vec!["https://bsky.network".to_string()]) 897 - } 898 - } 899 - 900 /// Parse a comma-separated environment variable into a `Vec<String>`, 901 /// trimming whitespace and dropping empty entries. 902 /// ··· 909 .collect()) 910 } 911 912 - // --------------------------------------------------------------------------- 913 - // Ripple (built-in gossip cache) 914 - // --------------------------------------------------------------------------- 915 - 916 #[derive(Debug, Config)] 917 - pub struct RippleConfig { 918 /// Address to bind the Ripple gossip protocol listener. 919 #[config(env = "RIPPLE_BIND", default = "0.0.0.0:0")] 920 pub bind_addr: String, 921 922 - /// Comma-separated list of seed peer addresses. 923 - #[config(env = "RIPPLE_PEERS")] 924 - pub peers: Option<String>, 925 926 /// Unique machine identifier. Auto-derived from hostname when not set. 927 #[config(env = "RIPPLE_MACHINE_ID")] ··· 936 pub cache_max_mb: usize, 937 } 938 939 - // --------------------------------------------------------------------------- 940 - // Scheduled tasks 941 - // --------------------------------------------------------------------------- 942 - 943 #[derive(Debug, Config)] 944 pub struct ScheduledConfig { 945 /// Interval in seconds between scheduled delete checks. 946 #[config(env = "SCHEDULED_DELETE_CHECK_INTERVAL_SECS", default = 3600)] 947 pub delete_check_interval_secs: u64, 948 } 949 - 950 - // --------------------------------------------------------------------------- 951 - // TOML template generation 952 - // --------------------------------------------------------------------------- 953 954 /// Generate a TOML configuration template with all available options, 955 /// defaults, and documentation comments.
··· 113 pub import: ImportConfig, 114 115 #[config(nested)] 116 pub scheduled: ScheduledConfig, 117 } 118 ··· 258 ); 259 } 260 261 + // -- cache ------------------------------------------------------------ 262 + match self.cache.backend.as_str() { 263 + "valkey" => { 264 + if self.cache.valkey_url.is_none() { 265 + errors.push( 266 + "cache.backend is \"valkey\" but cache.valkey_url (VALKEY_URL) \ 267 + is not set" 268 + .to_string(), 269 + ); 270 + } 271 + } 272 + "ripple" => {} 273 + other => { 274 + errors.push(format!( 275 + "cache.backend must be \"ripple\" or \"valkey\", got \"{other}\"" 276 + )); 277 + } 278 + } 279 + 280 if errors.is_empty() { 281 Ok(()) 282 } else { ··· 385 #[config(env = "SERVER_PORT", default = 3000)] 386 pub port: u16, 387 388 + /// List of domains for user handles. 389 + /// Defaults to the PDS hostname when not set. 390 + #[config(env = "PDS_USER_HANDLE_DOMAINS", parse_env = split_comma_list)] 391 + pub user_handle_domains: Option<Vec<String>>, 392 393 + /// List of domains available for user registration. 394 /// Defaults to the PDS hostname when not set. 395 #[config(env = "AVAILABLE_USER_DOMAINS", parse_env = split_comma_list)] 396 pub available_user_domains: Option<Vec<String>>, 397 398 + /// Enable PDS-hosted did:web identities. Hosting did:web requires a 399 + /// long-term commitment to serve DID documents; opt-in only. 400 + #[config(env = "ENABLE_PDS_HOSTED_DID_WEB", default = false)] 401 + pub enable_pds_hosted_did_web: bool, 402 403 /// When set to true, skip age-assurance birthday prompt for all accounts. 404 #[config(env = "PDS_AGE_ASSURANCE_OVERRIDE", default = false)] 405 pub age_assurance_override: bool, 406 407 /// Require an invite code for new account registration. 408 + #[config(env = "INVITE_CODE_REQUIRED", default = true)] 409 pub invite_code_required: bool, 410 411 /// Allow HTTP (non-TLS) proxy requests. Only useful during development. ··· 416 #[config(env = "DISABLE_RATE_LIMITING", default = false)] 417 pub disable_rate_limiting: bool, 418 419 + /// List of additional banned words for handle validation. 420 #[config(env = "PDS_BANNED_WORDS", parse_env = split_comma_list)] 421 pub banned_words: Option<Vec<String>>, 422 ··· 461 .unwrap_or_else(|| vec![self.hostname_without_port().to_string()]) 462 } 463 464 + /// Returns the user handle domains, falling back to `[hostname_without_port]`. 465 + pub fn user_handle_domain_list(&self) -> Vec<String> { 466 + self.user_handle_domains 467 .clone() 468 .unwrap_or_else(|| vec![self.hostname_without_port().to_string()]) 469 } 470 } 471 472 #[derive(Debug, Config)] 473 pub struct DatabaseConfig { 474 /// PostgreSQL connection URL. ··· 488 pub acquire_timeout_secs: u64, 489 } 490 491 #[derive(Config)] 492 pub struct SecretsConfig { 493 /// Secret used for signing JWTs. Must be at least 32 characters in ··· 585 } 586 } 587 588 #[derive(Debug, Config)] 589 pub struct StorageConfig { 590 /// Storage backend: `filesystem` or `s3`. ··· 604 pub s3_endpoint: Option<String>, 605 } 606 607 #[derive(Debug, Config)] 608 pub struct BackupConfig { 609 /// Enable automatic backups. ··· 631 pub interval_secs: u64, 632 } 633 634 #[derive(Debug, Config)] 635 pub struct CacheConfig { 636 + /// Cache backend: `ripple` (default, built-in gossip) or `valkey`. 637 + #[config(env = "CACHE_BACKEND", default = "ripple")] 638 + pub backend: String, 639 + 640 + /// Valkey / Redis connection URL. Required when `backend = "valkey"`. 641 #[config(env = "VALKEY_URL")] 642 pub valkey_url: Option<String>, 643 + 644 + #[config(nested)] 645 + pub ripple: RippleCacheConfig, 646 } 647 648 #[derive(Debug, Config)] 649 pub struct PlcConfig { 650 /// Base URL of the PLC directory. ··· 664 pub did_cache_ttl_secs: u64, 665 } 666 667 #[derive(Debug, Config)] 668 pub struct FirehoseConfig { 669 /// Size of the in-memory broadcast buffer for firehose events. ··· 678 /// Maximum number of lagged events before disconnecting a slow consumer. 679 #[config(env = "FIREHOSE_MAX_LAG", default = 5000)] 680 pub max_lag: u64, 681 + 682 + /// List of relay / crawler notification URLs. 683 + #[config(env = "CRAWLERS", parse_env = split_comma_list)] 684 + pub crawlers: Option<Vec<String>>, 685 } 686 687 + impl FirehoseConfig { 688 + /// Returns the list of crawler URLs, falling back to `["https://bsky.network"]` 689 + /// when none are configured. 690 + pub fn crawler_list(&self) -> Vec<String> { 691 + self.crawlers 692 + .clone() 693 + .unwrap_or_else(|| vec!["https://bsky.network".to_string()]) 694 + } 695 + } 696 697 #[derive(Debug, Config)] 698 pub struct EmailConfig { ··· 709 pub sendmail_path: String, 710 } 711 712 #[derive(Debug, Config)] 713 pub struct DiscordConfig { 714 /// Discord bot token. When unset, Discord integration is disabled. ··· 716 pub bot_token: Option<String>, 717 } 718 719 #[derive(Debug, Config)] 720 pub struct TelegramConfig { 721 /// Telegram bot token. When unset, Telegram integration is disabled. ··· 727 pub webhook_secret: Option<String>, 728 } 729 730 #[derive(Debug, Config)] 731 pub struct SignalConfig { 732 /// Path to the `signal-cli` binary. ··· 738 pub sender_number: Option<String>, 739 } 740 741 #[derive(Debug, Config)] 742 pub struct NotificationConfig { 743 /// Polling interval in milliseconds for the comms queue. ··· 749 pub batch_size: i64, 750 } 751 752 #[derive(Debug, Config)] 753 pub struct SsoConfig { 754 #[config(nested)] ··· 820 pub private_key: Option<String>, 821 } 822 823 #[derive(Debug, Config)] 824 pub struct ModerationConfig { 825 /// External report-handling service URL. ··· 831 pub report_service_did: Option<String>, 832 } 833 834 #[derive(Debug, Config)] 835 pub struct ImportConfig { 836 /// Whether the PDS accepts repo imports. ··· 850 pub skip_verification: bool, 851 } 852 853 /// Parse a comma-separated environment variable into a `Vec<String>`, 854 /// trimming whitespace and dropping empty entries. 855 /// ··· 862 .collect()) 863 } 864 865 #[derive(Debug, Config)] 866 + pub struct RippleCacheConfig { 867 /// Address to bind the Ripple gossip protocol listener. 868 #[config(env = "RIPPLE_BIND", default = "0.0.0.0:0")] 869 pub bind_addr: String, 870 871 + /// List of seed peer addresses. 872 + #[config(env = "RIPPLE_PEERS", parse_env = split_comma_list)] 873 + pub peers: Option<Vec<String>>, 874 875 /// Unique machine identifier. Auto-derived from hostname when not set. 876 #[config(env = "RIPPLE_MACHINE_ID")] ··· 885 pub cache_max_mb: usize, 886 } 887 888 #[derive(Debug, Config)] 889 pub struct ScheduledConfig { 890 /// Interval in seconds between scheduled delete checks. 891 #[config(env = "SCHEDULED_DELETE_CHECK_INTERVAL_SECS", default = 3600)] 892 pub delete_check_interval_secs: u64, 893 } 894 895 /// Generate a TOML configuration template with all available options, 896 /// defaults, and documentation comments.
crates/tranquil-infra/Cargo.toml

This file has not been changed.

crates/tranquil-infra/src/lib.rs

This file has not been changed.

crates/tranquil-pds/Cargo.toml

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/email.rs

This file has not been changed.

crates/tranquil-pds/src/api/admin/account/update.rs

This file has not been changed.

crates/tranquil-pds/src/api/delegation.rs

This file has not been changed.

crates/tranquil-pds/src/api/discord_webhook.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/account.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/did.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/plc/request.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/plc/submit.rs

This file has not been changed.

crates/tranquil-pds/src/api/moderation/mod.rs

This file has not been changed.

crates/tranquil-pds/src/api/notification_prefs.rs

This file has not been changed.

crates/tranquil-pds/src/api/proxy.rs

This file has not been changed.

crates/tranquil-pds/src/api/proxy_client.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/blob.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/import.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/meta.rs

This file has not been changed.

crates/tranquil-pds/src/api/repo/record/read.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/account_status.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/email.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/invite.rs

This file has not been changed.

+1 -1
crates/tranquil-pds/src/api/server/meta.rs
··· 27 ) 28 } 29 pub fn is_self_hosted_did_web_enabled() -> bool { 30 - tranquil_config::get().server.enable_self_hosted_did_web 31 } 32 33 pub async fn describe_server() -> impl IntoResponse {
··· 27 ) 28 } 29 pub fn is_self_hosted_did_web_enabled() -> bool { 30 + tranquil_config::get().server.enable_pds_hosted_did_web 31 } 32 33 pub async fn describe_server() -> impl IntoResponse {
crates/tranquil-pds/src/api/server/migration.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/passkey_account.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/password.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/session.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/totp.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/verify_email.rs

This file has not been changed.

crates/tranquil-pds/src/api/server/verify_token.rs

This file has not been changed.

crates/tranquil-pds/src/api/telegram_webhook.rs

This file has not been changed.

crates/tranquil-pds/src/appview/mod.rs

This file has not been changed.

crates/tranquil-pds/src/auth/service.rs

This file has not been changed.

crates/tranquil-pds/src/auth/verification_token.rs

This file has not been changed.

crates/tranquil-pds/src/comms/service.rs

This file has not been changed.

crates/tranquil-pds/src/config.rs

This file has not been changed.

+2 -3
crates/tranquil-pds/src/crawlers.rs
··· 41 self 42 } 43 44 - pub fn from_env() -> Option<Self> { 45 - let cfg = tranquil_config::get(); 46 let hostname = &cfg.server.hostname; 47 if hostname == "localhost" { 48 return None; 49 } 50 51 - let crawler_urls = cfg.crawlers.url_list(); 52 53 if crawler_urls.is_empty() { 54 return None;
··· 41 self 42 } 43 44 + pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> { 45 let hostname = &cfg.server.hostname; 46 if hostname == "localhost" { 47 return None; 48 } 49 50 + let crawler_urls = cfg.firehose.crawler_list(); 51 52 if crawler_urls.is_empty() { 53 return None;
+1 -1
crates/tranquil-pds/src/handle/mod.rs
··· 91 if !handle.contains('.') { 92 return true; 93 } 94 - let service_domains = tranquil_config::get().server.service_handle_domain_list(); 95 service_domains 96 .iter() 97 .any(|domain| handle.ends_with(&format!(".{}", domain)) || handle == domain)
··· 91 if !handle.contains('.') { 92 return true; 93 } 94 + let service_domains = tranquil_config::get().server.user_handle_domain_list(); 95 service_domains 96 .iter() 97 .any(|domain| handle.ends_with(&format!(".{}", domain)) || handle == domain)
crates/tranquil-pds/src/lib.rs

This file has not been changed.

+9 -7
crates/tranquil-pds/src/main.rs
··· 23 #[derive(Parser)] 24 #[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")] 25 struct Cli { 26 - /// Path to a TOML configuration file (also settable via TRANQUIL_CONFIG env var) 27 - #[arg(short, long, value_name = "FILE", env = "TRANQUIL_CONFIG")] 28 config: Option<PathBuf>, 29 30 #[command(subcommand)] ··· 135 let mut comms_service = CommsService::new(state.infra_repo.clone()); 136 let mut deferred_discord_endpoint: Option<(DiscordSender, String, String)> = None; 137 138 - if let Some(email_sender) = EmailSender::from_env() { 139 info!("Email comms enabled"); 140 comms_service = comms_service.register_sender(email_sender); 141 } else { 142 warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)"); 143 } 144 145 - if let Some(discord_sender) = DiscordSender::from_env() { 146 info!("Discord comms enabled"); 147 match discord_sender.resolve_bot_username().await { 148 Ok(username) => { ··· 186 comms_service = comms_service.register_sender(discord_sender); 187 } 188 189 - if let Some(telegram_sender) = TelegramSender::from_env() { 190 // Safe to unwrap: validated in TranquilConfig::validate() 191 let secret_token = tranquil_config::get() 192 .telegram ··· 215 comms_service = comms_service.register_sender(telegram_sender); 216 } 217 218 - if let Some(signal_sender) = SignalSender::from_env() { 219 info!("Signal comms enabled"); 220 comms_service = comms_service.register_sender(signal_sender); 221 } 222 223 let comms_handle = tokio::spawn(comms_service.run(shutdown.clone())); 224 225 - let crawlers_handle = if let Some(crawlers) = Crawlers::from_env() { 226 let crawlers = Arc::new( 227 crawlers.with_circuit_breaker(state.circuit_breakers.relay_notification.clone()), 228 );
··· 23 #[derive(Parser)] 24 #[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")] 25 struct Cli { 26 + /// Path to a TOML configuration file (also settable via TRANQUIL_PDS_CONFIG env var) 27 + #[arg(short, long, value_name = "FILE", env = "TRANQUIL_PDS_CONFIG")] 28 config: Option<PathBuf>, 29 30 #[command(subcommand)] ··· 135 let mut comms_service = CommsService::new(state.infra_repo.clone()); 136 let mut deferred_discord_endpoint: Option<(DiscordSender, String, String)> = None; 137 138 + let cfg = tranquil_config::get(); 139 + 140 + if let Some(email_sender) = EmailSender::from_config(cfg) { 141 info!("Email comms enabled"); 142 comms_service = comms_service.register_sender(email_sender); 143 } else { 144 warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)"); 145 } 146 147 + if let Some(discord_sender) = DiscordSender::from_config(cfg) { 148 info!("Discord comms enabled"); 149 match discord_sender.resolve_bot_username().await { 150 Ok(username) => { ··· 188 comms_service = comms_service.register_sender(discord_sender); 189 } 190 191 + if let Some(telegram_sender) = TelegramSender::from_config(cfg) { 192 // Safe to unwrap: validated in TranquilConfig::validate() 193 let secret_token = tranquil_config::get() 194 .telegram ··· 217 comms_service = comms_service.register_sender(telegram_sender); 218 } 219 220 + if let Some(signal_sender) = SignalSender::from_config(cfg) { 221 info!("Signal comms enabled"); 222 comms_service = comms_service.register_sender(signal_sender); 223 } 224 225 let comms_handle = tokio::spawn(comms_service.run(shutdown.clone())); 226 227 + let crawlers_handle = if let Some(crawlers) = Crawlers::from_config(cfg) { 228 let crawlers = Arc::new( 229 crawlers.with_circuit_breaker(state.circuit_breakers.relay_notification.clone()), 230 );
crates/tranquil-pds/src/moderation/mod.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/authorize.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/metadata.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/token/grants.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs

This file has not been changed.

crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs

This file has not been changed.

crates/tranquil-pds/src/plc/mod.rs

This file has not been changed.

crates/tranquil-pds/src/scheduled.rs

This file has not been changed.

crates/tranquil-pds/src/sso/config.rs

This file has not been changed.

crates/tranquil-pds/src/sso/endpoints.rs

This file has not been changed.

crates/tranquil-pds/src/state.rs

This file has not been changed.

crates/tranquil-pds/src/sync/subscribe_repos.rs

This file has not been changed.

crates/tranquil-pds/src/sync/verify.rs

This file has not been changed.

crates/tranquil-pds/src/util.rs

This file has not been changed.

crates/tranquil-ripple/Cargo.toml

This file has not been changed.

+4 -4
crates/tranquil-ripple/src/config.rs
··· 16 } 17 18 impl RippleConfig { 19 - pub fn from_env() -> Result<Self, RippleConfigError> { 20 - let ripple = &tranquil_config::get().ripple; 21 22 let bind_addr: SocketAddr = ripple 23 .bind_addr ··· 27 let seed_peers: Vec<SocketAddr> = ripple 28 .peers 29 .as_deref() 30 - .unwrap_or("") 31 - .split(',') 32 .filter(|s| !s.trim().is_empty()) 33 .map(|s| { 34 s.trim()
··· 16 } 17 18 impl RippleConfig { 19 + pub fn from_config() -> Result<Self, RippleConfigError> { 20 + let ripple = &tranquil_config::get().cache.ripple; 21 22 let bind_addr: SocketAddr = ripple 23 .bind_addr ··· 27 let seed_peers: Vec<SocketAddr> = ripple 28 .peers 29 .as_deref() 30 + .unwrap_or(&[]) 31 + .iter() 32 .filter(|s| !s.trim().is_empty()) 33 .map(|s| { 34 s.trim()
crates/tranquil-storage/Cargo.toml

This file has not been changed.

+21 -17
crates/tranquil-storage/src/lib.rs
··· 508 }) 509 } 510 511 - pub async fn from_env() -> Result<Self, StorageError> { 512 - let path = tranquil_config::get().storage.path.clone().ok_or_else(|| { 513 - StorageError::Other("storage.path (BLOB_STORAGE_PATH) not set".into()) 514 - })?; 515 - Self::new(path).await 516 - } 517 - 518 fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> { 519 validate_key(key)?; 520 Ok(split_cid_path(key).map_or_else( ··· 659 }) 660 } 661 662 - pub async fn from_env() -> Result<Self, StorageError> { 663 - let path = tranquil_config::get().backup.path.clone().ok_or_else(|| { 664 - StorageError::Other("backup.path (BACKUP_STORAGE_PATH) not set".into()) 665 - })?; 666 - Self::new(path).await 667 - } 668 - 669 fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> { 670 validate_key(key)?; 671 Ok(self.base_path.join(key)) ··· 730 } 731 _ => { 732 tracing::info!("Initializing filesystem blob storage"); 733 - FilesystemBlobStorage::from_env() 734 .await 735 .unwrap_or_else(|e| { 736 panic!( ··· 777 ); 778 None 779 } 780 - _ => FilesystemBackupStorage::from_env().await.map_or_else( 781 |e| { 782 tracing::error!( 783 "Failed to initialize filesystem backup storage: {}. \ ··· 791 tracing::info!("Initialized filesystem backup storage"); 792 Some(Arc::new(storage) as Arc<dyn BackupStorage>) 793 }, 794 - ), 795 } 796 } 797
··· 508 }) 509 } 510 511 fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> { 512 validate_key(key)?; 513 Ok(split_cid_path(key).map_or_else( ··· 652 }) 653 } 654 655 fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> { 656 validate_key(key)?; 657 Ok(self.base_path.join(key)) ··· 716 } 717 _ => { 718 tracing::info!("Initializing filesystem blob storage"); 719 + let path = cfg.storage.path.clone().unwrap_or_else(|| { 720 + panic!( 721 + "storage.path (BLOB_STORAGE_PATH) not set. \ 722 + Set BLOB_STORAGE_PATH to a valid directory path." 723 + ); 724 + }); 725 + FilesystemBlobStorage::new(path) 726 .await 727 .unwrap_or_else(|e| { 728 panic!( ··· 769 ); 770 None 771 } 772 + _ => { 773 + let path = match cfg.backup.path.clone() { 774 + Some(p) => p, 775 + None => { 776 + tracing::error!( 777 + "backup.path (BACKUP_STORAGE_PATH) not set. \ 778 + Backups will be disabled." 779 + ); 780 + return None; 781 + } 782 + }; 783 + FilesystemBackupStorage::new(path).await.map_or_else( 784 |e| { 785 tracing::error!( 786 "Failed to initialize filesystem backup storage: {}. \ ··· 794 tracing::info!("Initialized filesystem backup storage"); 795 Some(Arc::new(storage) as Arc<dyn BackupStorage>) 796 }, 797 + ) 798 + } 799 } 800 } 801
docker-compose.prod.yaml

This file has not been changed.

docker-compose.yaml

This file has not been changed.

+8
docs/install-containers.md
··· 125 openssl rand -base64 48 126 ``` 127 128 ## Install quadlet definitions 129 130 Copy the quadlet files from the repository: ··· 274 ```sh 275 openssl rand -base64 48 276 ``` 277 278 ## Set up compose and nginx 279
··· 125 openssl rand -base64 48 126 ``` 127 128 + > **Note:** Every config option can also be set via environment variables 129 + > (see comments in `example.toml`). Environment variables always take 130 + > precedence over the config file. 131 + 132 ## Install quadlet definitions 133 134 Copy the quadlet files from the repository: ··· 278 ```sh 279 openssl rand -base64 48 280 ``` 281 + 282 + > **Note:** Every config option can also be set via environment variables 283 + > (see comments in `example.toml`). Environment variables always take 284 + > precedence over the config file. 285 286 ## Set up compose and nginx 287
+5
docs/install-debian.md
··· 82 openssl rand -base64 48 83 ``` 84 85 You can validate your configuration before starting the service: 86 ```bash 87 /usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate
··· 82 openssl rand -base64 48 83 ``` 84 85 + > **Note:** Every config option can also be set via environment variables 86 + > (see comments in `example.toml`). Environment variables always take 87 + > precedence over the config file. You can also pass the config file path 88 + > via the `TRANQUIL_PDS_CONFIG` env var instead of `--config`. 89 + 90 You can validate your configuration before starting the service: 91 ```bash 92 /usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate
+3 -1
docs/install-kubernetes.md
··· 16 - `PDS_HOSTNAME` - your PDS hostname (without protocol) 17 - `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48` 18 - `CRAWLERS` - typically `https://bsky.network` 19 - and more, check the example.toml for all options. Environment variables can override any TOML values. 20 21 Health check: `GET /xrpc/_health` 22
··· 16 - `PDS_HOSTNAME` - your PDS hostname (without protocol) 17 - `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48` 18 - `CRAWLERS` - typically `https://bsky.network` 19 + 20 + and more, check the example.toml for all options. Environment variables can override any TOML value. 21 + You can also point to a config file via the `TRANQUIL_PDS_CONFIG` env var. 22 23 Health check: `GET /xrpc/_health` 24
+392 -156
example.toml
··· 1 - # copy this file to /etc/tranquil-pds/config.toml and edit as needed 2 # 3 - # environment variables always take precedence over values in this file 4 # 5 - # Generate secrets with: openssl rand -base64 48 6 7 - [server] 8 - # The public-facing hostname of the PDS (used in DID documents, JWTs, etc.) 9 - hostname = "localhost" 10 - 11 # Address to bind the HTTP server to. 12 - host = "127.0.0.1" 13 14 # Port to bind the HTTP server to. 15 - port = 3000 16 17 - # Comma-separated list of domains that are considered service handle domains. 18 # Defaults to the PDS hostname when not set. 19 - # service_handle_domains = "pds.example.com" 20 21 - # Comma-separated list of domains available for user registration. 22 # Defaults to the PDS hostname when not set. 23 - # available_user_domains = "example.com" 24 25 - # Enable self-hosted did:web identities. 26 - # Hosting did:web requires a long-term commitment to serve DID documents. 27 - # Set to false if you don't want to offer this option. 28 - enable_self_hosted_did_web = true 29 30 - # When true, skip the age-assurance birthday prompt for all accounts. 31 - # Enable this if you have separately assured the ages of your users 32 - # (e.g., through your own age verification process). 33 - age_assurance_override = false 34 35 # Require an invite code for new account registration. 36 - invite_code_required = false 37 38 - # Allow HTTP (non-TLS) proxy requests. Only for development. 39 - allow_http_proxy = false 40 41 - # Disable all rate limiting. Should NEVER be enabled in production. 42 - disable_rate_limiting = false 43 44 - # list of additional banned words for handle validation. 45 - # banned_words = [""] 46 47 - # URL to a privacy policy page (optional, shown in describeServer). 48 - # privacy_policy_url = "https://example.com/privacy" 49 50 - # URL to terms of service page (optional, shown in describeServer). 51 - # terms_of_service_url = "https://example.com/terms" 52 53 - # Operator contact email address (optional, shown in describeServer). 54 - # contact_email = "admin@example.com" 55 56 - # Maximum allowed blob/body size in bytes (default 10 GiB). 57 - # Make sure your nginx client_max_body_size matches or exceeds this value. 58 - max_blob_size = 10_737_418_240 59 60 [database] 61 - # PostgreSQL connection URL (REQUIRED). 62 - url = "postgres://postgres:postgres@localhost:5432/pds" 63 64 # Maximum number of connections in the pool. 65 - max_connections = 100 66 67 # Minimum number of idle connections kept in the pool. 68 - min_connections = 10 69 70 # Timeout in seconds when acquiring a connection from the pool. 71 - acquire_timeout_secs = 10 72 73 [secrets] 74 - # These MUST be set in production (minimum 32 characters each). 75 - # Generate with: openssl rand -base64 48 76 77 - # Server-wide secret for OAuth token signing (HS256). 78 - # jwt_secret = "your-secure-random-string-at-least-32-chars" 79 80 - # Secret for DPoP proof validation. 81 - # dpop_secret = "your-secure-random-string-at-least-32-chars" 82 83 - # Key for encrypting user signing keys at rest (AES-256-GCM). 84 - # master_key = "your-secure-random-string-at-least-32-chars" 85 - 86 # PLC rotation key (DID key). If not set, user-level keys are used. 87 - # plc_rotation_key = "did:key:..." 88 89 # Allow insecure/test secrets. NEVER enable in production. 90 - # Set to true ONLY in development to allow default/weak secrets. 91 - allow_insecure = false 92 93 [storage] 94 - # Backend: "filesystem" (default) or "s3". 95 - backend = "filesystem" 96 97 - # For filesystem backend: path on disk for blob storage. 98 - path = "/var/lib/tranquil/blobs" 99 100 - # For S3 backend: bucket name. 101 - # s3_bucket = "pds-blobs" 102 103 # Custom S3 endpoint URL (for MinIO, R2, etc.). 104 - # s3_endpoint = "http://localhost:9000" 105 106 [backup] 107 - # Enable automatic repo backups. 108 - enabled = true 109 110 - # Backup storage backend: "filesystem" (default) or "s3". 111 - backend = "filesystem" 112 113 - # For filesystem backend: path on disk for backups. 114 - path = "/var/lib/tranquil/backups" 115 116 - # For S3 backend: bucket name. 117 - # s3_bucket = "pds-backups" 118 119 # Number of backup revisions to keep per account. 120 - retention_count = 7 121 122 - # Seconds between backup runs (default: 24 hours). 123 - interval_secs = 86400 124 125 [cache] 126 - # valkey_url = "redis://localhost:6379" 127 128 [plc] 129 # Base URL of the PLC directory. 130 - directory_url = "https://plc.directory" 131 132 # HTTP request timeout in seconds. 133 - timeout_secs = 10 134 135 # TCP connect timeout in seconds. 136 - connect_timeout_secs = 5 137 138 # Seconds to cache DID documents in memory. 139 - did_cache_ttl_secs = 300 140 141 [firehose] 142 # Size of the in-memory broadcast buffer for firehose events. 143 - buffer_size = 10_000 144 145 - # How many hours of historical events to replay for cursor-based connections. 146 - backfill_hours = 72 147 148 # Maximum number of lagged events before disconnecting a slow consumer. 149 - max_lag = 5000 150 151 - [notifications] 152 - # polling interval in milliseconds for the comms queue. 153 - poll_interval_ms = 1000 154 155 - # number of notifications to process per batch. 156 - batch_size = 100 157 - 158 [email] 159 - # sender email address. when unset, email sending is disabled. 160 - # from_address = "noreply@example.com" 161 162 - # display name used in the from headerw 163 - from_name = "Tranquil PDS" 164 165 - # Path to the sendmail binary. 166 - sendmail_path = "/usr/sbin/sendmail" 167 168 [discord] 169 - # discord bot token. when unset, Discord integration is disabled. 170 - # bot_token = "bot-token" 171 172 [telegram] 173 # Telegram bot token. When unset, Telegram integration is disabled. 174 - # bot_token = "bot-token" 175 176 # Secret token for incoming webhook verification. 177 - # Required when bot_token is set. 178 - # webhook_secret = "random-secret" 179 180 [signal] 181 - # Path to the signal-cli binary. 182 - cli_path = "/usr/local/bin/signal-cli" 183 184 # Sender phone number. When unset, Signal integration is disabled. 185 - # sender_number = "+1234567890" 186 187 - ## SSO 188 189 [sso.github] 190 - enabled = false 191 - # client_id = "" 192 - # client_secret = "" 193 194 [sso.discord] 195 - enabled = false 196 - # client_id = "" 197 - # client_secret = "" 198 199 [sso.google] 200 - enabled = false 201 - # client_id = "" 202 - # client_secret = "" 203 204 [sso.gitlab] 205 - enabled = false 206 - # client_id = "" 207 - # client_secret = "" 208 - # issuer = "https://gitlab.com" 209 210 [sso.oidc] 211 - enabled = false 212 - # client_id = "" 213 - # client_secret = "" 214 - # issuer = "https://your-identity-provider.com" 215 - # display_name = "Custom Provider" 216 217 [sso.apple] 218 - enabled = false 219 - # Services ID from Apple Developer Portal 220 - # client_id = "com.example.signin" 221 222 - # 10-character Team ID 223 - # team_id = "XXXXXXXXXX" 224 225 - # Key ID from portal 226 - # key_id = "XXXXXXXXXX" 227 228 - # private_key = "" 229 230 [moderation] 231 - # report_service_url = "https://mod.bsky.app" 232 - # report_service_did = "did:plc:ar7c4by46qjdydhdevvrndac" 233 234 [import] 235 # Whether the PDS accepts repo imports. 236 - accepting = true 237 238 # Maximum allowed import archive size in bytes (default 1 GiB). 239 - max_size = 1_073_741_824 240 241 # Maximum number of blocks allowed in an import. 242 - max_blocks = 500_000 243 244 # Skip CAR verification during import. Only for development/debugging. 245 - skip_verification = false 246 247 - [crawlers] 248 - # list of relay URLs to notify via requestCrawl. 249 - urls = ["https://bsky.network"] 250 - 251 - [ripple] 252 - # Address to bind the Ripple gossip protocol listener. 253 - bind_addr = "0.0.0.0:0" 254 - 255 - # Comma-separated list of seed peer addresses (for multi-node). 256 - # peers = "10.0.0.2:7890,10.0.0.3:7890" 257 - 258 - # Unique machine identifier. Auto-derived from hostname when not set. 259 - # machine_id = 1 260 - 261 - # Gossip protocol interval in milliseconds. 262 - gossip_interval_ms = 200 263 - 264 - # Maximum cache size in megabytes. 265 - cache_max_mb = 256 266 - 267 [scheduled] 268 - # How often to check for scheduled account deletions (default: 1 hour). 269 - delete_check_interval_secs = 3600
··· 1 + [server] 2 + # Public hostname of the PDS (e.g. `pds.example.com`). 3 # 4 + # Can also be specified via environment variable `PDS_HOSTNAME`. 5 # 6 + # Required! This value must be specified. 7 + #hostname = 8 9 # Address to bind the HTTP server to. 10 + # 11 + # Can also be specified via environment variable `SERVER_HOST`. 12 + # 13 + # Default value: "127.0.0.1" 14 + #host = "127.0.0.1" 15 16 # Port to bind the HTTP server to. 17 + # 18 + # Can also be specified via environment variable `SERVER_PORT`. 19 + # 20 + # Default value: 3000 21 + #port = 3000 22 23 + # List of domains for user handles. 24 # Defaults to the PDS hostname when not set. 25 + # 26 + # Can also be specified via environment variable `PDS_USER_HANDLE_DOMAINS`. 27 + #user_handle_domains = 28 29 + # List of domains available for user registration. 30 # Defaults to the PDS hostname when not set. 31 + # 32 + # Can also be specified via environment variable `AVAILABLE_USER_DOMAINS`. 33 + #available_user_domains = 34 35 + # Enable PDS-hosted did:web identities. Hosting did:web requires a 36 + # long-term commitment to serve DID documents; opt-in only. 37 + # 38 + # Can also be specified via environment variable `ENABLE_PDS_HOSTED_DID_WEB`. 39 + # 40 + # Default value: false 41 + #enable_pds_hosted_did_web = false 42 43 + # When set to true, skip age-assurance birthday prompt for all accounts. 44 + # 45 + # Can also be specified via environment variable `PDS_AGE_ASSURANCE_OVERRIDE`. 46 + # 47 + # Default value: false 48 + #age_assurance_override = false 49 50 # Require an invite code for new account registration. 51 + # 52 + # Can also be specified via environment variable `INVITE_CODE_REQUIRED`. 53 + # 54 + # Default value: true 55 + #invite_code_required = true 56 57 + # Allow HTTP (non-TLS) proxy requests. Only useful during development. 58 + # 59 + # Can also be specified via environment variable `ALLOW_HTTP_PROXY`. 60 + # 61 + # Default value: false 62 + #allow_http_proxy = false 63 64 + # Disable all rate limiting. Should only be used in testing. 65 + # 66 + # Can also be specified via environment variable `DISABLE_RATE_LIMITING`. 67 + # 68 + # Default value: false 69 + #disable_rate_limiting = false 70 71 + # List of additional banned words for handle validation. 72 + # 73 + # Can also be specified via environment variable `PDS_BANNED_WORDS`. 74 + #banned_words = 75 76 + # URL to a privacy policy page. 77 + # 78 + # Can also be specified via environment variable `PRIVACY_POLICY_URL`. 79 + #privacy_policy_url = 80 81 + # URL to terms of service page. 82 + # 83 + # Can also be specified via environment variable `TERMS_OF_SERVICE_URL`. 84 + #terms_of_service_url = 85 86 + # Operator contact email address. 87 + # 88 + # Can also be specified via environment variable `CONTACT_EMAIL`. 89 + #contact_email = 90 91 + # Maximum allowed blob size in bytes (default 10 GiB). 92 + # 93 + # Can also be specified via environment variable `MAX_BLOB_SIZE`. 94 + # 95 + # Default value: 10737418240 96 + #max_blob_size = 10737418240 97 98 [database] 99 + # PostgreSQL connection URL. 100 + # 101 + # Can also be specified via environment variable `DATABASE_URL`. 102 + # 103 + # Required! This value must be specified. 104 + #url = 105 106 # Maximum number of connections in the pool. 107 + # 108 + # Can also be specified via environment variable `DATABASE_MAX_CONNECTIONS`. 109 + # 110 + # Default value: 100 111 + #max_connections = 100 112 113 # Minimum number of idle connections kept in the pool. 114 + # 115 + # Can also be specified via environment variable `DATABASE_MIN_CONNECTIONS`. 116 + # 117 + # Default value: 10 118 + #min_connections = 10 119 120 # Timeout in seconds when acquiring a connection from the pool. 121 + # 122 + # Can also be specified via environment variable `DATABASE_ACQUIRE_TIMEOUT_SECS`. 123 + # 124 + # Default value: 10 125 + #acquire_timeout_secs = 10 126 127 [secrets] 128 + # Secret used for signing JWTs. Must be at least 32 characters in 129 + # production. 130 + # 131 + # Can also be specified via environment variable `JWT_SECRET`. 132 + #jwt_secret = 133 134 + # Secret used for DPoP proof validation. Must be at least 32 characters 135 + # in production. 136 + # 137 + # Can also be specified via environment variable `DPOP_SECRET`. 138 + #dpop_secret = 139 140 + # Master key used for key-encryption and HKDF derivation. Must be at 141 + # least 32 characters in production. 142 + # 143 + # Can also be specified via environment variable `MASTER_KEY`. 144 + #master_key = 145 146 # PLC rotation key (DID key). If not set, user-level keys are used. 147 + # 148 + # Can also be specified via environment variable `PLC_ROTATION_KEY`. 149 + #plc_rotation_key = 150 151 # Allow insecure/test secrets. NEVER enable in production. 152 + # 153 + # Can also be specified via environment variable `TRANQUIL_PDS_ALLOW_INSECURE_SECRETS`. 154 + # 155 + # Default value: false 156 + #allow_insecure = false 157 158 [storage] 159 + # Storage backend: `filesystem` or `s3`. 160 + # 161 + # Can also be specified via environment variable `BLOB_STORAGE_BACKEND`. 162 + # 163 + # Default value: "filesystem" 164 + #backend = "filesystem" 165 166 + # Path on disk for the filesystem blob backend. 167 + # 168 + # Can also be specified via environment variable `BLOB_STORAGE_PATH`. 169 + #path = 170 171 + # S3 bucket name for blob storage. 172 + # 173 + # Can also be specified via environment variable `S3_BUCKET`. 174 + #s3_bucket = 175 176 # Custom S3 endpoint URL (for MinIO, R2, etc.). 177 + # 178 + # Can also be specified via environment variable `S3_ENDPOINT`. 179 + #s3_endpoint = 180 181 [backup] 182 + # Enable automatic backups. 183 + # 184 + # Can also be specified via environment variable `BACKUP_ENABLED`. 185 + # 186 + # Default value: true 187 + #enabled = true 188 189 + # Backup storage backend: `filesystem` or `s3`. 190 + # 191 + # Can also be specified via environment variable `BACKUP_STORAGE_BACKEND`. 192 + # 193 + # Default value: "filesystem" 194 + #backend = "filesystem" 195 196 + # Path on disk for the filesystem backup backend. 197 + # 198 + # Can also be specified via environment variable `BACKUP_STORAGE_PATH`. 199 + #path = 200 201 + # S3 bucket name for backups. 202 + # 203 + # Can also be specified via environment variable `BACKUP_S3_BUCKET`. 204 + #s3_bucket = 205 206 # Number of backup revisions to keep per account. 207 + # 208 + # Can also be specified via environment variable `BACKUP_RETENTION_COUNT`. 209 + # 210 + # Default value: 7 211 + #retention_count = 7 212 213 + # Seconds between backup runs. 214 + # 215 + # Can also be specified via environment variable `BACKUP_INTERVAL_SECS`. 216 + # 217 + # Default value: 86400 218 + #interval_secs = 86400 219 220 [cache] 221 + # Cache backend: `ripple` (default, built-in gossip) or `valkey`. 222 + # 223 + # Can also be specified via environment variable `CACHE_BACKEND`. 224 + # 225 + # Default value: "ripple" 226 + #backend = "ripple" 227 228 + # Valkey / Redis connection URL. Required when `backend = "valkey"`. 229 + # 230 + # Can also be specified via environment variable `VALKEY_URL`. 231 + #valkey_url = 232 + 233 + [cache.ripple] 234 + # Address to bind the Ripple gossip protocol listener. 235 + # 236 + # Can also be specified via environment variable `RIPPLE_BIND`. 237 + # 238 + # Default value: "0.0.0.0:0" 239 + #bind_addr = "0.0.0.0:0" 240 + 241 + # List of seed peer addresses. 242 + # 243 + # Can also be specified via environment variable `RIPPLE_PEERS`. 244 + #peers = 245 + 246 + # Unique machine identifier. Auto-derived from hostname when not set. 247 + # 248 + # Can also be specified via environment variable `RIPPLE_MACHINE_ID`. 249 + #machine_id = 250 + 251 + # Gossip protocol interval in milliseconds. 252 + # 253 + # Can also be specified via environment variable `RIPPLE_GOSSIP_INTERVAL_MS`. 254 + # 255 + # Default value: 200 256 + #gossip_interval_ms = 200 257 + 258 + # Maximum cache size in megabytes. 259 + # 260 + # Can also be specified via environment variable `RIPPLE_CACHE_MAX_MB`. 261 + # 262 + # Default value: 256 263 + #cache_max_mb = 256 264 + 265 [plc] 266 # Base URL of the PLC directory. 267 + # 268 + # Can also be specified via environment variable `PLC_DIRECTORY_URL`. 269 + # 270 + # Default value: "https://plc.directory" 271 + #directory_url = "https://plc.directory" 272 273 # HTTP request timeout in seconds. 274 + # 275 + # Can also be specified via environment variable `PLC_TIMEOUT_SECS`. 276 + # 277 + # Default value: 10 278 + #timeout_secs = 10 279 280 # TCP connect timeout in seconds. 281 + # 282 + # Can also be specified via environment variable `PLC_CONNECT_TIMEOUT_SECS`. 283 + # 284 + # Default value: 5 285 + #connect_timeout_secs = 5 286 287 # Seconds to cache DID documents in memory. 288 + # 289 + # Can also be specified via environment variable `DID_CACHE_TTL_SECS`. 290 + # 291 + # Default value: 300 292 + #did_cache_ttl_secs = 300 293 294 [firehose] 295 # Size of the in-memory broadcast buffer for firehose events. 296 + # 297 + # Can also be specified via environment variable `FIREHOSE_BUFFER_SIZE`. 298 + # 299 + # Default value: 10000 300 + #buffer_size = 10000 301 302 + # How many hours of historical events to replay for cursor-based 303 + # firehose connections. 304 + # 305 + # Can also be specified via environment variable `FIREHOSE_BACKFILL_HOURS`. 306 + # 307 + # Default value: 72 308 + #backfill_hours = 72 309 310 # Maximum number of lagged events before disconnecting a slow consumer. 311 + # 312 + # Can also be specified via environment variable `FIREHOSE_MAX_LAG`. 313 + # 314 + # Default value: 5000 315 + #max_lag = 5000 316 317 + # List of relay / crawler notification URLs. 318 + # 319 + # Can also be specified via environment variable `CRAWLERS`. 320 + #crawlers = 321 322 [email] 323 + # Sender email address. When unset, email sending is disabled. 324 + # 325 + # Can also be specified via environment variable `MAIL_FROM_ADDRESS`. 326 + #from_address = 327 328 + # Display name used in the `From` header. 329 + # 330 + # Can also be specified via environment variable `MAIL_FROM_NAME`. 331 + # 332 + # Default value: "Tranquil PDS" 333 + #from_name = "Tranquil PDS" 334 335 + # Path to the `sendmail` binary. 336 + # 337 + # Can also be specified via environment variable `SENDMAIL_PATH`. 338 + # 339 + # Default value: "/usr/sbin/sendmail" 340 + #sendmail_path = "/usr/sbin/sendmail" 341 342 [discord] 343 + # Discord bot token. When unset, Discord integration is disabled. 344 + # 345 + # Can also be specified via environment variable `DISCORD_BOT_TOKEN`. 346 + #bot_token = 347 348 [telegram] 349 # Telegram bot token. When unset, Telegram integration is disabled. 350 + # 351 + # Can also be specified via environment variable `TELEGRAM_BOT_TOKEN`. 352 + #bot_token = 353 354 # Secret token for incoming webhook verification. 355 + # 356 + # Can also be specified via environment variable `TELEGRAM_WEBHOOK_SECRET`. 357 + #webhook_secret = 358 359 [signal] 360 + # Path to the `signal-cli` binary. 361 + # 362 + # Can also be specified via environment variable `SIGNAL_CLI_PATH`. 363 + # 364 + # Default value: "/usr/local/bin/signal-cli" 365 + #cli_path = "/usr/local/bin/signal-cli" 366 367 # Sender phone number. When unset, Signal integration is disabled. 368 + # 369 + # Can also be specified via environment variable `SIGNAL_SENDER_NUMBER`. 370 + #sender_number = 371 372 + [notifications] 373 + # Polling interval in milliseconds for the comms queue. 374 + # 375 + # Can also be specified via environment variable `NOTIFICATION_POLL_INTERVAL_MS`. 376 + # 377 + # Default value: 1000 378 + #poll_interval_ms = 1000 379 380 + # Number of notifications to process per batch. 381 + # 382 + # Can also be specified via environment variable `NOTIFICATION_BATCH_SIZE`. 383 + # 384 + # Default value: 100 385 + #batch_size = 100 386 + 387 + [sso] 388 [sso.github] 389 + # Default value: false 390 + #enabled = false 391 392 + #client_id = 393 + 394 + #client_secret = 395 + 396 + #display_name = 397 + 398 [sso.discord] 399 + # Default value: false 400 + #enabled = false 401 402 + #client_id = 403 + 404 + #client_secret = 405 + 406 + #display_name = 407 + 408 [sso.google] 409 + # Default value: false 410 + #enabled = false 411 412 + #client_id = 413 + 414 + #client_secret = 415 + 416 + #display_name = 417 + 418 [sso.gitlab] 419 + # Default value: false 420 + #enabled = false 421 422 + #client_id = 423 + 424 + #client_secret = 425 + 426 + #issuer = 427 + 428 + #display_name = 429 + 430 [sso.oidc] 431 + # Default value: false 432 + #enabled = false 433 434 + #client_id = 435 + 436 + #client_secret = 437 + 438 + #issuer = 439 + 440 + #display_name = 441 + 442 [sso.apple] 443 + # Can also be specified via environment variable `SSO_APPLE_ENABLED`. 444 + # Default value: false 445 + #enabled = false 446 447 + # Can also be specified via environment variable `SSO_APPLE_CLIENT_ID`. 448 + #client_id = 449 450 + # Can also be specified via environment variable `SSO_APPLE_TEAM_ID`. 451 + #team_id = 452 453 + # Can also be specified via environment variable `SSO_APPLE_KEY_ID`. 454 + #key_id = 455 456 + # Can also be specified via environment variable `SSO_APPLE_PRIVATE_KEY`. 457 + #private_key = 458 + 459 [moderation] 460 + # External report-handling service URL. 461 + # 462 + # Can also be specified via environment variable `REPORT_SERVICE_URL`. 463 + #report_service_url = 464 465 + # DID of the external report-handling service. 466 + # 467 + # Can also be specified via environment variable `REPORT_SERVICE_DID`. 468 + #report_service_did = 469 + 470 [import] 471 # Whether the PDS accepts repo imports. 472 + # 473 + # Can also be specified via environment variable `ACCEPTING_REPO_IMPORTS`. 474 + # 475 + # Default value: true 476 + #accepting = true 477 478 # Maximum allowed import archive size in bytes (default 1 GiB). 479 + # 480 + # Can also be specified via environment variable `MAX_IMPORT_SIZE`. 481 + # 482 + # Default value: 1073741824 483 + #max_size = 1073741824 484 485 # Maximum number of blocks allowed in an import. 486 + # 487 + # Can also be specified via environment variable `MAX_IMPORT_BLOCKS`. 488 + # 489 + # Default value: 500000 490 + #max_blocks = 500000 491 492 # Skip CAR verification during import. Only for development/debugging. 493 + # 494 + # Can also be specified via environment variable `SKIP_IMPORT_VERIFICATION`. 495 + # 496 + # Default value: false 497 + #skip_verification = false 498 499 [scheduled] 500 + # Interval in seconds between scheduled delete checks. 501 + # 502 + # Can also be specified via environment variable `SCHEDULED_DELETE_CHECK_INTERVAL_SECS`. 503 + # 504 + # Default value: 3600 505 + #delete_check_interval_secs = 3600
+2 -2
.env.example
··· 131 # Account Registration 132 # ============================================================================= 133 # Require invite codes for registration 134 - # INVITE_CODE_REQUIRED=false 135 # Comma-separated list of available user domains 136 # AVAILABLE_USER_DOMAINS=example.com 137 # Enable self-hosted did:web identities (default: true) 138 # Hosting did:web requires a long-term commitment to serve DID documents. 139 # Set to false if you don't want to offer this option. 140 - # ENABLE_SELF_HOSTED_DID_WEB=true 141 # ============================================================================= 142 # Server Metadata (returned by describeServer) 143 # =============================================================================
··· 131 # Account Registration 132 # ============================================================================= 133 # Require invite codes for registration 134 + # INVITE_CODE_REQUIRED=true 135 # Comma-separated list of available user domains 136 # AVAILABLE_USER_DOMAINS=example.com 137 # Enable self-hosted did:web identities (default: true) 138 # Hosting did:web requires a long-term commitment to serve DID documents. 139 # Set to false if you don't want to offer this option. 140 + # ENABLE_PDS_HOSTED_DID_WEB=false 141 # ============================================================================= 142 # Server Metadata (returned by describeServer) 143 # =============================================================================
+4 -1
README.md
··· 24 25 ## Configuration 26 27 - See `.env.example` for all configuration options. 28 29 ## Development 30
··· 24 25 ## Configuration 26 27 + See `example.toml` for all configuration options. 28 + 29 + > [!NOTE] 30 + > 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. 31 32 ## Development 33

History

4 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
c66fe45e
refactor: toml config
expand 0 comments
pull request successfully merged
1 commit
expand
1b32bf4b
refactor: toml config
expand 0 comments
1 commit
expand
21b7ee92
refactor: toml config
expand 1 comment

oookay finally got a chance to try this out and noticed some usability mehs

the blob storage paths and the backup storage paths should have default values. doesnt make much sense to me to not have that.

validate should probably have a --ignore-secrets flag or similar? since most people will probably want to set the secrets in an env file and not in the config file. necessary for the nix module too if we want that to validate the config during build

anywhere possible errors from trying to load the config are printed to the user should also have some more handling to actually print the error properly. as https://github.com/LukasKalbertodt/confique/blob/main/src/error.rs#L9 mentions just printing it doesnt actually do that which ends up giving very cryptic and non-specific messages to the user just saying loading the config failed without giving proper reasons why. just doing e:# seemed fine for now for me locally. in the future we'll probably want to walk the sources properly but it can wait

1 commit
expand
13867bba
refactor: toml config
expand 1 comment

overall looks really good! i only really have some nitpicks about naming and default values.

server.service_handle_domains should be server.user_handle_domains imo. i never quite liked the "service handle domain" name. its confusing imo. and describeServer calls them user domains so i think we should align with that

server.enable_self_hosted_did_web makes it sound like its the opposite of what it is. should be server.enable_pds_hosted_did_web instead. also imo we should default this to false? given the consequences of having this enabled it should be opt-in imo

server.invite_code_required should probably default to true? thats what ref impl does and i think thats sensible

crawlers.urls should be firehose.crawlers imo. i think it makes sense to keep it with the rest of the sync related config (or have a layer of nesting more and have sync.crawlers and sync.firehose. sounds messy tho)

all of ripple.* should probably go under cache.ripple.* (as well as add a cache.backend option) since ripple is an in-house in-process replacement for valkey

also perhaps make it clear in the docs that you can set config options with env vars too? + the config cli flag and env var

idk how i feel about the config static being a OnceLock and not a LazyLock and all the panicing with init() and get(). but i understand getting it to work with a LazyLock is annoying due to the fallibility of config loading. probably going to explore how to handle that in the future. not going to block this PR on that