crates
tranquil-auth
tranquil-cache
tranquil-comms
tranquil-config
tranquil-infra
tranquil-pds
src
api
admin
identity
moderation
repo
server
appview
auth
comms
handle
moderation
oauth
plc
sso
sync
tranquil-ripple
tranquil-storage
docs
···
131
131
# Account Registration
132
132
# =============================================================================
133
133
# Require invite codes for registration
134
134
-
# INVITE_CODE_REQUIRED=false
134
134
+
# INVITE_CODE_REQUIRED=true
135
135
# Comma-separated list of available user domains
136
136
# AVAILABLE_USER_DOMAINS=example.com
137
137
# Enable self-hosted did:web identities (default: true)
138
138
# Hosting did:web requires a long-term commitment to serve DID documents.
139
139
# Set to false if you don't want to offer this option.
140
140
-
# ENABLE_SELF_HOSTED_DID_WEB=true
140
140
+
# ENABLE_PDS_HOSTED_DID_WEB=false
141
141
# =============================================================================
142
142
# Server Metadata (returned by describeServer)
143
143
# =============================================================================
···
105
105
]
106
106
107
107
[[package]]
108
108
+
name = "anstream"
109
109
+
version = "0.6.21"
110
110
+
source = "registry+https://github.com/rust-lang/crates.io-index"
111
111
+
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
112
112
+
dependencies = [
113
113
+
"anstyle",
114
114
+
"anstyle-parse",
115
115
+
"anstyle-query",
116
116
+
"anstyle-wincon",
117
117
+
"colorchoice",
118
118
+
"is_terminal_polyfill",
119
119
+
"utf8parse",
120
120
+
]
121
121
+
122
122
+
[[package]]
123
123
+
name = "anstyle"
124
124
+
version = "1.0.13"
125
125
+
source = "registry+https://github.com/rust-lang/crates.io-index"
126
126
+
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
127
127
+
128
128
+
[[package]]
129
129
+
name = "anstyle-parse"
130
130
+
version = "0.2.7"
131
131
+
source = "registry+https://github.com/rust-lang/crates.io-index"
132
132
+
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
133
133
+
dependencies = [
134
134
+
"utf8parse",
135
135
+
]
136
136
+
137
137
+
[[package]]
138
138
+
name = "anstyle-query"
139
139
+
version = "1.1.5"
140
140
+
source = "registry+https://github.com/rust-lang/crates.io-index"
141
141
+
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
142
142
+
dependencies = [
143
143
+
"windows-sys 0.61.2",
144
144
+
]
145
145
+
146
146
+
[[package]]
147
147
+
name = "anstyle-wincon"
148
148
+
version = "3.0.11"
149
149
+
source = "registry+https://github.com/rust-lang/crates.io-index"
150
150
+
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
151
151
+
dependencies = [
152
152
+
"anstyle",
153
153
+
"once_cell_polyfill",
154
154
+
"windows-sys 0.61.2",
155
155
+
]
156
156
+
157
157
+
[[package]]
108
158
name = "anyhow"
109
159
version = "1.0.100"
110
160
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1217
1267
]
1218
1268
1219
1269
[[package]]
1270
1270
+
name = "clap"
1271
1271
+
version = "4.5.58"
1272
1272
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1273
1273
+
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
1274
1274
+
dependencies = [
1275
1275
+
"clap_builder",
1276
1276
+
"clap_derive",
1277
1277
+
]
1278
1278
+
1279
1279
+
[[package]]
1280
1280
+
name = "clap_builder"
1281
1281
+
version = "4.5.58"
1282
1282
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1283
1283
+
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
1284
1284
+
dependencies = [
1285
1285
+
"anstream",
1286
1286
+
"anstyle",
1287
1287
+
"clap_lex",
1288
1288
+
"strsim",
1289
1289
+
]
1290
1290
+
1291
1291
+
[[package]]
1292
1292
+
name = "clap_derive"
1293
1293
+
version = "4.5.55"
1294
1294
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1295
1295
+
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
1296
1296
+
dependencies = [
1297
1297
+
"heck 0.5.0",
1298
1298
+
"proc-macro2",
1299
1299
+
"quote",
1300
1300
+
"syn 2.0.111",
1301
1301
+
]
1302
1302
+
1303
1303
+
[[package]]
1304
1304
+
name = "clap_lex"
1305
1305
+
version = "1.0.0"
1306
1306
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1307
1307
+
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
1308
1308
+
1309
1309
+
[[package]]
1220
1310
name = "cmake"
1221
1311
version = "0.1.57"
1222
1312
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1241
1331
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
1242
1332
1243
1333
[[package]]
1334
1334
+
name = "colorchoice"
1335
1335
+
version = "1.0.4"
1336
1336
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1337
1337
+
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
1338
1338
+
1339
1339
+
[[package]]
1244
1340
name = "combine"
1245
1341
version = "4.6.7"
1246
1342
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1281
1377
]
1282
1378
1283
1379
[[package]]
1380
1380
+
name = "confique"
1381
1381
+
version = "0.4.0"
1382
1382
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1383
1383
+
checksum = "06b4f5ec222421e22bb0a8cbaa36b1d2b50fd45cdd30c915ded34108da78b29f"
1384
1384
+
dependencies = [
1385
1385
+
"confique-macro",
1386
1386
+
"serde",
1387
1387
+
"toml",
1388
1388
+
]
1389
1389
+
1390
1390
+
[[package]]
1391
1391
+
name = "confique-macro"
1392
1392
+
version = "0.0.13"
1393
1393
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1394
1394
+
checksum = "e4d1754680cd218e7bcb4c960cc9bae3444b5197d64563dccccfdf83cab9e1a7"
1395
1395
+
dependencies = [
1396
1396
+
"heck 0.5.0",
1397
1397
+
"proc-macro2",
1398
1398
+
"quote",
1399
1399
+
"syn 2.0.111",
1400
1400
+
]
1401
1401
+
1402
1402
+
[[package]]
1284
1403
name = "const-oid"
1285
1404
version = "0.9.6"
1286
1405
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2762
2881
"libc",
2763
2882
"percent-encoding",
2764
2883
"pin-project-lite",
2765
2765
-
"socket2 0.5.10",
2884
2884
+
"socket2 0.6.2",
2766
2885
"system-configuration",
2767
2886
"tokio",
2768
2887
"tower-service",
···
3058
3177
"tokio",
3059
3178
"unsigned-varint 0.7.2",
3060
3179
]
3180
3180
+
3181
3181
+
[[package]]
3182
3182
+
name = "is_terminal_polyfill"
3183
3183
+
version = "1.70.2"
3184
3184
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3185
3185
+
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
3061
3186
3062
3187
[[package]]
3063
3188
name = "itertools"
···
3716
3841
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
3717
3842
3718
3843
[[package]]
3844
3844
+
name = "once_cell_polyfill"
3845
3845
+
version = "1.70.2"
3846
3846
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3847
3847
+
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
3848
3848
+
3849
3849
+
[[package]]
3719
3850
name = "opaque-debug"
3720
3851
version = "0.3.1"
3721
3852
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4209
4340
"quinn-udp",
4210
4341
"rustc-hash",
4211
4342
"rustls 0.23.35",
4212
4212
-
"socket2 0.5.10",
4343
4343
+
"socket2 0.6.2",
4213
4344
"thiserror 2.0.17",
4214
4345
"tokio",
4215
4346
"tracing",
···
4246
4377
"cfg_aliases",
4247
4378
"libc",
4248
4379
"once_cell",
4249
4249
-
"socket2 0.5.10",
4380
4380
+
"socket2 0.6.2",
4250
4381
"tracing",
4251
4382
"windows-sys 0.60.2",
4252
4383
]
···
4907
5038
"proc-macro2",
4908
5039
"quote",
4909
5040
"syn 2.0.111",
5041
5041
+
]
5042
5042
+
5043
5043
+
[[package]]
5044
5044
+
name = "serde_spanned"
5045
5045
+
version = "1.0.4"
5046
5046
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5047
5047
+
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
5048
5048
+
dependencies = [
5049
5049
+
"serde_core",
4910
5050
]
4911
5051
4912
5052
[[package]]
···
5709
5849
]
5710
5850
5711
5851
[[package]]
5852
5852
+
name = "toml"
5853
5853
+
version = "0.9.12+spec-1.1.0"
5854
5854
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5855
5855
+
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
5856
5856
+
dependencies = [
5857
5857
+
"indexmap 2.12.1",
5858
5858
+
"serde_core",
5859
5859
+
"serde_spanned",
5860
5860
+
"toml_datetime",
5861
5861
+
"toml_parser",
5862
5862
+
"toml_writer",
5863
5863
+
"winnow",
5864
5864
+
]
5865
5865
+
5866
5866
+
[[package]]
5867
5867
+
name = "toml_datetime"
5868
5868
+
version = "0.7.5+spec-1.1.0"
5869
5869
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5870
5870
+
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
5871
5871
+
dependencies = [
5872
5872
+
"serde_core",
5873
5873
+
]
5874
5874
+
5875
5875
+
[[package]]
5876
5876
+
name = "toml_parser"
5877
5877
+
version = "1.0.7+spec-1.1.0"
5878
5878
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5879
5879
+
checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1"
5880
5880
+
dependencies = [
5881
5881
+
"winnow",
5882
5882
+
]
5883
5883
+
5884
5884
+
[[package]]
5885
5885
+
name = "toml_writer"
5886
5886
+
version = "1.0.6+spec-1.1.0"
5887
5887
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5888
5888
+
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
5889
5889
+
5890
5890
+
[[package]]
5712
5891
name = "tonic"
5713
5892
version = "0.14.2"
5714
5893
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5908
6087
"sha2",
5909
6088
"subtle",
5910
6089
"totp-rs",
6090
6090
+
"tranquil-config",
5911
6091
"tranquil-crypto",
5912
6092
"urlencoding",
5913
6093
"uuid",
···
5922
6102
"redis",
5923
6103
"tokio-util",
5924
6104
"tracing",
6105
6105
+
"tranquil-config",
5925
6106
"tranquil-infra",
5926
6107
"tranquil-ripple",
5927
6108
]
···
5936
6117
"serde_json",
5937
6118
"thiserror 2.0.17",
5938
6119
"tokio",
6120
6120
+
"tranquil-config",
5939
6121
"tranquil-db-traits",
5940
6122
"uuid",
6123
6123
+
]
6124
6124
+
6125
6125
+
[[package]]
6126
6126
+
name = "tranquil-config"
6127
6127
+
version = "0.2.1"
6128
6128
+
dependencies = [
6129
6129
+
"confique",
6130
6130
+
"serde",
5941
6131
]
5942
6132
5943
6133
[[package]]
···
5997
6187
"bytes",
5998
6188
"futures",
5999
6189
"thiserror 2.0.17",
6190
6190
+
"tranquil-config",
6000
6191
]
6001
6192
6002
6193
[[package]]
···
6041
6232
"chrono",
6042
6233
"ciborium",
6043
6234
"cid",
6235
6235
+
"clap",
6044
6236
"ctor",
6045
6237
"dotenvy",
6046
6238
"ed25519-dalek",
···
6091
6283
"tranquil-auth",
6092
6284
"tranquil-cache",
6093
6285
"tranquil-comms",
6286
6286
+
"tranquil-config",
6094
6287
"tranquil-crypto",
6095
6288
"tranquil-db",
6096
6289
"tranquil-db-traits",
···
6139
6332
"tokio-util",
6140
6333
"tracing",
6141
6334
"tracing-subscriber",
6335
6335
+
"tranquil-config",
6142
6336
"tranquil-infra",
6143
6337
"uuid",
6144
6338
]
···
6171
6365
"sha2",
6172
6366
"tokio",
6173
6367
"tracing",
6368
6368
+
"tranquil-config",
6174
6369
"tranquil-infra",
6175
6370
"uuid",
6176
6371
]
···
6355
6550
version = "1.0.4"
6356
6551
source = "registry+https://github.com/rust-lang/crates.io-index"
6357
6552
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
6553
6553
+
6554
6554
+
[[package]]
6555
6555
+
name = "utf8parse"
6556
6556
+
version = "0.2.2"
6557
6557
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6558
6558
+
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
6358
6559
6359
6560
[[package]]
6360
6561
name = "uuid"
···
6929
7130
version = "0.53.1"
6930
7131
source = "registry+https://github.com/rust-lang/crates.io-index"
6931
7132
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
7133
7133
+
7134
7134
+
[[package]]
7135
7135
+
name = "winnow"
7136
7136
+
version = "0.7.14"
7137
7137
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7138
7138
+
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
6932
7139
6933
7140
[[package]]
6934
7141
name = "winreg"
···
2
2
resolver = "2"
3
3
members = [
4
4
"crates/tranquil-types",
5
5
+
"crates/tranquil-config",
5
6
"crates/tranquil-infra",
6
7
"crates/tranquil-crypto",
7
8
"crates/tranquil-storage",
···
24
25
25
26
[workspace.dependencies]
26
27
tranquil-types = { path = "crates/tranquil-types" }
28
28
+
tranquil-config = { path = "crates/tranquil-config" }
27
29
tranquil-infra = { path = "crates/tranquil-infra" }
28
30
tranquil-crypto = { path = "crates/tranquil-crypto" }
29
31
tranquil-storage = { path = "crates/tranquil-storage" }
···
52
54
bytes = "1.11"
53
55
chrono = { version = "0.4", features = ["serde"] }
54
56
cid = "0.11"
57
57
+
clap = { version = "4", features = ["derive", "env"] }
58
58
+
confique = { version = "0.4", features = ["toml"] }
55
59
dotenvy = "0.15"
56
60
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
57
61
foca = { version = "1", features = ["bincode-codec", "tracing"] }
···
24
24
25
25
## Configuration
26
26
27
27
-
See `.env.example` for all configuration options.
27
27
+
See `example.toml` for all configuration options.
28
28
+
29
29
+
> [!NOTE]
30
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.
28
31
29
32
## Development
30
33
···
5
5
license.workspace = true
6
6
7
7
[dependencies]
8
8
+
tranquil-config = { workspace = true }
8
9
tranquil-crypto = { workspace = true }
9
10
10
11
anyhow = { workspace = true }
···
127
127
let jti = uuid::Uuid::new_v4().to_string();
128
128
129
129
let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| {
130
130
-
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
130
130
+
tranquil_config::try_get()
131
131
+
.map(|c| c.server.hostname.clone())
132
132
+
.unwrap_or_else(|| "localhost".to_string())
131
133
});
132
134
133
135
let claims = Claims {
···
253
255
sub: did.to_owned(),
254
256
aud: format!(
255
257
"did:web:{}",
256
256
-
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
258
258
+
tranquil_config::try_get()
259
259
+
.map(|c| c.server.hostname.clone())
260
260
+
.unwrap_or_else(|| "localhost".to_string())
257
261
),
258
262
exp: expiration,
259
263
iat: Utc::now().timestamp(),
···
9
9
valkey = ["dep:redis"]
10
10
11
11
[dependencies]
12
12
+
tranquil-config = { workspace = true }
12
13
tranquil-infra = { workspace = true }
13
14
tranquil-ripple = { workspace = true }
14
15
···
172
172
pub async fn create_cache(
173
173
shutdown: tokio_util::sync::CancellationToken,
174
174
) -> (Arc<dyn Cache>, Arc<dyn DistributedRateLimiter>) {
175
175
+
let cache_cfg = tranquil_config::try_get().map(|c| &c.cache);
176
176
+
let backend = cache_cfg.map(|c| c.backend.as_str()).unwrap_or("ripple");
177
177
+
let valkey_url = cache_cfg.and_then(|c| c.valkey_url.as_deref());
178
178
+
175
179
#[cfg(feature = "valkey")]
176
176
-
if let Ok(url) = std::env::var("VALKEY_URL") {
177
177
-
match ValkeyCache::new(&url).await {
178
178
-
Ok(cache) => {
179
179
-
tracing::info!("using valkey cache at {url}");
180
180
-
let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection()));
181
181
-
return (Arc::new(cache), rate_limiter);
182
182
-
}
183
183
-
Err(e) => {
184
184
-
tracing::warn!("failed to connect to valkey: {e}. falling back to ripple.");
180
180
+
if backend == "valkey" {
181
181
+
if let Some(url) = valkey_url {
182
182
+
match ValkeyCache::new(url).await {
183
183
+
Ok(cache) => {
184
184
+
tracing::info!("using valkey cache at {url}");
185
185
+
let rate_limiter = Arc::new(RedisRateLimiter::new(cache.connection()));
186
186
+
return (Arc::new(cache), rate_limiter);
187
187
+
}
188
188
+
Err(e) => {
189
189
+
tracing::warn!("failed to connect to valkey: {e}. falling back to ripple.");
190
190
+
}
185
191
}
192
192
+
} else {
193
193
+
tracing::warn!("cache.backend is \"valkey\" but VALKEY_URL is not set. using ripple.");
186
194
}
187
195
}
188
196
189
197
#[cfg(not(feature = "valkey"))]
190
190
-
if std::env::var("VALKEY_URL").is_ok() {
198
198
+
if backend == "valkey" {
191
199
tracing::warn!(
192
192
-
"VALKEY_URL is set but binary was compiled without valkey feature. using ripple."
200
200
+
"cache.backend is \"valkey\" but binary was compiled without valkey feature. using ripple."
193
201
);
194
202
}
195
203
196
196
-
match tranquil_ripple::RippleConfig::from_env() {
204
204
+
match tranquil_ripple::RippleConfig::from_config() {
197
205
Ok(config) => {
198
206
let peer_count = config.seed_peers.len();
199
207
match tranquil_ripple::RippleEngine::start(config, shutdown).await {
···
205
213
(cache, rate_limiter)
206
214
}
207
215
Err(e) => {
208
208
-
tracing::error!("ripple engine failed to start: {e}. running without cache.");
216
216
+
tracing::error!("ripple engine failed to start: {e:#}. running without cache.");
209
217
(Arc::new(NoOpCache), Arc::new(NoOpRateLimiter))
210
218
}
211
219
}
212
220
}
213
221
Err(e) => {
214
214
-
tracing::error!("ripple config error: {e}. running without cache.");
222
222
+
tracing::error!("ripple config error: {e:#}. running without cache.");
215
223
(Arc::new(NoOpCache), Arc::new(NoOpRateLimiter))
216
224
}
217
225
}
···
5
5
license.workspace = true
6
6
7
7
[dependencies]
8
8
+
tranquil-config = { workspace = true }
9
9
+
8
10
async-trait = { workspace = true }
9
11
base64 = { workspace = true }
10
12
reqwest = { workspace = true }
···
112
112
}
113
113
114
114
impl EmailSender {
115
115
-
pub fn new(from_address: String, from_name: String) -> Self {
115
115
+
pub fn new(from_address: String, from_name: String, sendmail_path: String) -> Self {
116
116
Self {
117
117
from_address,
118
118
from_name,
119
119
-
sendmail_path: std::env::var("SENDMAIL_PATH")
120
120
-
.unwrap_or_else(|_| "/usr/sbin/sendmail".to_string()),
119
119
+
sendmail_path,
121
120
}
122
121
}
123
122
124
124
-
pub fn from_env() -> Option<Self> {
125
125
-
let from_address = std::env::var("MAIL_FROM_ADDRESS").ok()?;
126
126
-
let from_name =
127
127
-
std::env::var("MAIL_FROM_NAME").unwrap_or_else(|_| "Tranquil PDS".to_string());
128
128
-
Some(Self::new(from_address, from_name))
123
123
+
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
124
124
+
let from_address = cfg.email.from_address.clone()?;
125
125
+
let from_name = cfg.email.from_name.clone();
126
126
+
let sendmail_path = cfg.email.sendmail_path.clone();
127
127
+
Some(Self::new(from_address, from_name, sendmail_path))
129
128
}
130
129
131
130
pub fn format_email(&self, notification: &QueuedComms) -> String {
···
190
189
}
191
190
}
192
191
193
193
-
pub fn from_env() -> Option<Self> {
194
194
-
let bot_token = std::env::var("DISCORD_BOT_TOKEN").ok()?;
192
192
+
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
193
193
+
let bot_token = cfg.discord.bot_token.clone()?;
195
194
Some(Self::new(bot_token))
196
195
}
197
196
···
454
453
}
455
454
}
456
455
457
457
-
pub fn from_env() -> Option<Self> {
458
458
-
let bot_token = std::env::var("TELEGRAM_BOT_TOKEN").ok()?;
456
456
+
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
457
457
+
let bot_token = cfg.telegram.bot_token.clone()?;
459
458
Some(Self::new(bot_token))
460
459
}
461
460
···
586
585
}
587
586
}
588
587
589
589
-
pub fn from_env() -> Option<Self> {
590
590
-
let signal_cli_path = std::env::var("SIGNAL_CLI_PATH")
591
591
-
.unwrap_or_else(|_| "/usr/local/bin/signal-cli".to_string());
592
592
-
let sender_number = std::env::var("SIGNAL_SENDER_NUMBER").ok()?;
588
588
+
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
589
589
+
let signal_cli_path = cfg.signal.cli_path.clone();
590
590
+
let sender_number = cfg.signal.sender_number.clone()?;
593
591
Some(Self::new(signal_cli_path, sender_number))
594
592
}
595
593
}
···
1
1
+
[package]
2
2
+
name = "tranquil-config"
3
3
+
version.workspace = true
4
4
+
edition.workspace = true
5
5
+
license.workspace = true
6
6
+
7
7
+
[dependencies]
8
8
+
confique = { workspace = true }
9
9
+
serde = { workspace = true }
···
1
1
+
use confique::Config;
2
2
+
use std::fmt;
3
3
+
use std::path::PathBuf;
4
4
+
use std::sync::OnceLock;
5
5
+
6
6
+
static CONFIG: OnceLock<TranquilConfig> = OnceLock::new();
7
7
+
8
8
+
/// Errors discovered during configuration validation.
9
9
+
#[derive(Debug)]
10
10
+
pub struct ConfigError {
11
11
+
pub errors: Vec<String>,
12
12
+
}
13
13
+
14
14
+
impl fmt::Display for ConfigError {
15
15
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16
16
+
writeln!(f, "configuration validation failed:")?;
17
17
+
for err in &self.errors {
18
18
+
writeln!(f, " - {err}")?;
19
19
+
}
20
20
+
Ok(())
21
21
+
}
22
22
+
}
23
23
+
24
24
+
impl std::error::Error for ConfigError {}
25
25
+
26
26
+
/// Initialize the global configuration. Must be called once at startup before
27
27
+
/// any other code accesses the configuration. Panics if called more than once.
28
28
+
pub fn init(config: TranquilConfig) {
29
29
+
CONFIG
30
30
+
.set(config)
31
31
+
.expect("tranquil-config: configuration already initialized");
32
32
+
}
33
33
+
34
34
+
/// Returns a reference to the global configuration.
35
35
+
/// Panics if [`init`] has not been called yet.
36
36
+
pub fn get() -> &'static TranquilConfig {
37
37
+
CONFIG
38
38
+
.get()
39
39
+
.expect("tranquil-config: not initialized - call tranquil_config::init() first")
40
40
+
}
41
41
+
42
42
+
/// Returns a reference to the global configuration if it has been initialized.
43
43
+
pub fn try_get() -> Option<&'static TranquilConfig> {
44
44
+
CONFIG.get()
45
45
+
}
46
46
+
47
47
+
/// Load configuration from an optional TOML file path, with environment
48
48
+
/// variable overrides applied on top. Fields annotated with `#[config(env)]`
49
49
+
/// are read from the corresponding environment variables when the `.env()`
50
50
+
/// layer is active.
51
51
+
///
52
52
+
/// Precedence (highest to lowest):
53
53
+
/// 1. Environment variables
54
54
+
/// 2. TOML config file (if provided)
55
55
+
/// 3. Built-in defaults
56
56
+
pub fn load(config_path: Option<&PathBuf>) -> Result<TranquilConfig, confique::Error> {
57
57
+
let mut builder = TranquilConfig::builder().env();
58
58
+
if let Some(path) = config_path {
59
59
+
builder = builder.file(path);
60
60
+
}
61
61
+
builder.file("/etc/tranquil-pds/config.toml").load()
62
62
+
}
63
63
+
64
64
+
// Root configuration
65
65
+
#[derive(Debug, Config)]
66
66
+
pub struct TranquilConfig {
67
67
+
#[config(nested)]
68
68
+
pub server: ServerConfig,
69
69
+
70
70
+
#[config(nested)]
71
71
+
pub database: DatabaseConfig,
72
72
+
73
73
+
#[config(nested)]
74
74
+
pub secrets: SecretsConfig,
75
75
+
76
76
+
#[config(nested)]
77
77
+
pub storage: StorageConfig,
78
78
+
79
79
+
#[config(nested)]
80
80
+
pub backup: BackupConfig,
81
81
+
82
82
+
#[config(nested)]
83
83
+
pub cache: CacheConfig,
84
84
+
85
85
+
#[config(nested)]
86
86
+
pub plc: PlcConfig,
87
87
+
88
88
+
#[config(nested)]
89
89
+
pub firehose: FirehoseConfig,
90
90
+
91
91
+
#[config(nested)]
92
92
+
pub email: EmailConfig,
93
93
+
94
94
+
#[config(nested)]
95
95
+
pub discord: DiscordConfig,
96
96
+
97
97
+
#[config(nested)]
98
98
+
pub telegram: TelegramConfig,
99
99
+
100
100
+
#[config(nested)]
101
101
+
pub signal: SignalConfig,
102
102
+
103
103
+
#[config(nested)]
104
104
+
pub notifications: NotificationConfig,
105
105
+
106
106
+
#[config(nested)]
107
107
+
pub sso: SsoConfig,
108
108
+
109
109
+
#[config(nested)]
110
110
+
pub moderation: ModerationConfig,
111
111
+
112
112
+
#[config(nested)]
113
113
+
pub import: ImportConfig,
114
114
+
115
115
+
#[config(nested)]
116
116
+
pub scheduled: ScheduledConfig,
117
117
+
}
118
118
+
119
119
+
impl TranquilConfig {
120
120
+
/// Validate cross-field constraints that cannot be expressed through
121
121
+
/// confique's declarative defaults alone. Call this once after loading
122
122
+
/// the configuration and before [`init`].
123
123
+
///
124
124
+
/// Returns `Ok(())` when the configuration is consistent, or a
125
125
+
/// [`ConfigError`] listing every problem found.
126
126
+
pub fn validate(&self, ignore_secrets: bool) -> Result<(), ConfigError> {
127
127
+
let mut errors = Vec::new();
128
128
+
129
129
+
// -- secrets ----------------------------------------------------------
130
130
+
if !ignore_secrets && !self.secrets.allow_insecure && !cfg!(test) {
131
131
+
if let Some(ref s) = self.secrets.jwt_secret {
132
132
+
if s.len() < 32 {
133
133
+
errors.push(
134
134
+
"secrets.jwt_secret (JWT_SECRET) must be at least 32 characters"
135
135
+
.to_string(),
136
136
+
);
137
137
+
}
138
138
+
} else {
139
139
+
errors.push(
140
140
+
"secrets.jwt_secret (JWT_SECRET) is required in production \
141
141
+
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
142
142
+
.to_string(),
143
143
+
);
144
144
+
}
145
145
+
146
146
+
if let Some(ref s) = self.secrets.dpop_secret {
147
147
+
if s.len() < 32 {
148
148
+
errors.push(
149
149
+
"secrets.dpop_secret (DPOP_SECRET) must be at least 32 characters"
150
150
+
.to_string(),
151
151
+
);
152
152
+
}
153
153
+
} else {
154
154
+
errors.push(
155
155
+
"secrets.dpop_secret (DPOP_SECRET) is required in production \
156
156
+
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
157
157
+
.to_string(),
158
158
+
);
159
159
+
}
160
160
+
161
161
+
if let Some(ref s) = self.secrets.master_key {
162
162
+
if s.len() < 32 {
163
163
+
errors.push(
164
164
+
"secrets.master_key (MASTER_KEY) must be at least 32 characters"
165
165
+
.to_string(),
166
166
+
);
167
167
+
}
168
168
+
} else {
169
169
+
errors.push(
170
170
+
"secrets.master_key (MASTER_KEY) is required in production \
171
171
+
(set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development)"
172
172
+
.to_string(),
173
173
+
);
174
174
+
}
175
175
+
}
176
176
+
177
177
+
// -- telegram ---------------------------------------------------------
178
178
+
if self.telegram.bot_token.is_some() && self.telegram.webhook_secret.is_none() {
179
179
+
errors.push(
180
180
+
"telegram.bot_token is set but telegram.webhook_secret is missing; \
181
181
+
both are required for secure Telegram integration"
182
182
+
.to_string(),
183
183
+
);
184
184
+
}
185
185
+
186
186
+
// -- blob storage -----------------------------------------------------
187
187
+
match self.storage.backend.as_str() {
188
188
+
"s3" => {
189
189
+
if self.storage.s3_bucket.is_none() {
190
190
+
errors.push(
191
191
+
"storage.backend is \"s3\" but storage.s3_bucket (S3_BUCKET) \
192
192
+
is not set"
193
193
+
.to_string(),
194
194
+
);
195
195
+
}
196
196
+
}
197
197
+
"filesystem" => {}
198
198
+
other => {
199
199
+
errors.push(format!(
200
200
+
"storage.backend must be \"filesystem\" or \"s3\", got \"{other}\""
201
201
+
));
202
202
+
}
203
203
+
}
204
204
+
205
205
+
// -- backup storage ---------------------------------------------------
206
206
+
if self.backup.enabled {
207
207
+
match self.backup.backend.as_str() {
208
208
+
"s3" => {
209
209
+
if self.backup.s3_bucket.is_none() {
210
210
+
errors.push(
211
211
+
"backup.backend is \"s3\" but backup.s3_bucket \
212
212
+
(BACKUP_S3_BUCKET) is not set"
213
213
+
.to_string(),
214
214
+
);
215
215
+
}
216
216
+
}
217
217
+
"filesystem" => {}
218
218
+
other => {
219
219
+
errors.push(format!(
220
220
+
"backup.backend must be \"filesystem\" or \"s3\", got \"{other}\""
221
221
+
));
222
222
+
}
223
223
+
}
224
224
+
}
225
225
+
226
226
+
// -- SSO providers ----------------------------------------------------
227
227
+
self.validate_sso_provider("sso.github", &self.sso.github, &mut errors);
228
228
+
self.validate_sso_provider("sso.google", &self.sso.google, &mut errors);
229
229
+
self.validate_sso_discord(&mut errors);
230
230
+
self.validate_sso_with_issuer("sso.gitlab", &self.sso.gitlab, &mut errors);
231
231
+
self.validate_sso_with_issuer("sso.oidc", &self.sso.oidc, &mut errors);
232
232
+
self.validate_sso_apple(&mut errors);
233
233
+
234
234
+
// -- moderation -------------------------------------------------------
235
235
+
let has_url = self.moderation.report_service_url.is_some();
236
236
+
let has_did = self.moderation.report_service_did.is_some();
237
237
+
if has_url != has_did {
238
238
+
errors.push(
239
239
+
"moderation.report_service_url and moderation.report_service_did \
240
240
+
must both be set or both be unset"
241
241
+
.to_string(),
242
242
+
);
243
243
+
}
244
244
+
245
245
+
// -- cache ------------------------------------------------------------
246
246
+
match self.cache.backend.as_str() {
247
247
+
"valkey" => {
248
248
+
if self.cache.valkey_url.is_none() {
249
249
+
errors.push(
250
250
+
"cache.backend is \"valkey\" but cache.valkey_url (VALKEY_URL) \
251
251
+
is not set"
252
252
+
.to_string(),
253
253
+
);
254
254
+
}
255
255
+
}
256
256
+
"ripple" => {}
257
257
+
other => {
258
258
+
errors.push(format!(
259
259
+
"cache.backend must be \"ripple\" or \"valkey\", got \"{other}\""
260
260
+
));
261
261
+
}
262
262
+
}
263
263
+
264
264
+
if errors.is_empty() {
265
265
+
Ok(())
266
266
+
} else {
267
267
+
Err(ConfigError { errors })
268
268
+
}
269
269
+
}
270
270
+
271
271
+
fn validate_sso_provider(&self, prefix: &str, p: &SsoProviderConfig, errors: &mut Vec<String>) {
272
272
+
if p.enabled {
273
273
+
if p.client_id.is_none() {
274
274
+
errors.push(format!(
275
275
+
"{prefix}.client_id is required when {prefix}.enabled = true"
276
276
+
));
277
277
+
}
278
278
+
if p.client_secret.is_none() {
279
279
+
errors.push(format!(
280
280
+
"{prefix}.client_secret is required when {prefix}.enabled = true"
281
281
+
));
282
282
+
}
283
283
+
}
284
284
+
}
285
285
+
286
286
+
fn validate_sso_discord(&self, errors: &mut Vec<String>) {
287
287
+
let p = &self.sso.discord;
288
288
+
if p.enabled {
289
289
+
if p.client_id.is_none() {
290
290
+
errors.push(
291
291
+
"sso.discord.client_id is required when sso.discord.enabled = true".to_string(),
292
292
+
);
293
293
+
}
294
294
+
if p.client_secret.is_none() {
295
295
+
errors.push(
296
296
+
"sso.discord.client_secret is required when sso.discord.enabled = true"
297
297
+
.to_string(),
298
298
+
);
299
299
+
}
300
300
+
}
301
301
+
}
302
302
+
303
303
+
fn validate_sso_with_issuer(
304
304
+
&self,
305
305
+
prefix: &str,
306
306
+
p: &SsoProviderWithIssuerConfig,
307
307
+
errors: &mut Vec<String>,
308
308
+
) {
309
309
+
if p.enabled {
310
310
+
if p.client_id.is_none() {
311
311
+
errors.push(format!(
312
312
+
"{prefix}.client_id is required when {prefix}.enabled = true"
313
313
+
));
314
314
+
}
315
315
+
if p.client_secret.is_none() {
316
316
+
errors.push(format!(
317
317
+
"{prefix}.client_secret is required when {prefix}.enabled = true"
318
318
+
));
319
319
+
}
320
320
+
if p.issuer.is_none() {
321
321
+
errors.push(format!(
322
322
+
"{prefix}.issuer is required when {prefix}.enabled = true"
323
323
+
));
324
324
+
}
325
325
+
}
326
326
+
}
327
327
+
328
328
+
fn validate_sso_apple(&self, errors: &mut Vec<String>) {
329
329
+
let p = &self.sso.apple;
330
330
+
if p.enabled {
331
331
+
if p.client_id.is_none() {
332
332
+
errors.push(
333
333
+
"sso.apple.client_id is required when sso.apple.enabled = true".to_string(),
334
334
+
);
335
335
+
}
336
336
+
if p.team_id.is_none() {
337
337
+
errors.push(
338
338
+
"sso.apple.team_id is required when sso.apple.enabled = true".to_string(),
339
339
+
);
340
340
+
}
341
341
+
if p.key_id.is_none() {
342
342
+
errors
343
343
+
.push("sso.apple.key_id is required when sso.apple.enabled = true".to_string());
344
344
+
}
345
345
+
if p.private_key.is_none() {
346
346
+
errors.push(
347
347
+
"sso.apple.private_key is required when sso.apple.enabled = true".to_string(),
348
348
+
);
349
349
+
}
350
350
+
}
351
351
+
}
352
352
+
}
353
353
+
354
354
+
// ---------------------------------------------------------------------------
355
355
+
// Server
356
356
+
// ---------------------------------------------------------------------------
357
357
+
358
358
+
#[derive(Debug, Config)]
359
359
+
pub struct ServerConfig {
360
360
+
/// Public hostname of the PDS (e.g. `pds.example.com`).
361
361
+
#[config(env = "PDS_HOSTNAME")]
362
362
+
pub hostname: String,
363
363
+
364
364
+
/// Address to bind the HTTP server to.
365
365
+
#[config(env = "SERVER_HOST", default = "127.0.0.1")]
366
366
+
pub host: String,
367
367
+
368
368
+
/// Port to bind the HTTP server to.
369
369
+
#[config(env = "SERVER_PORT", default = 3000)]
370
370
+
pub port: u16,
371
371
+
372
372
+
/// List of domains for user handles.
373
373
+
/// Defaults to the PDS hostname when not set.
374
374
+
#[config(env = "PDS_USER_HANDLE_DOMAINS", parse_env = split_comma_list)]
375
375
+
pub user_handle_domains: Option<Vec<String>>,
376
376
+
377
377
+
/// List of domains available for user registration.
378
378
+
/// Defaults to the PDS hostname when not set.
379
379
+
#[config(env = "AVAILABLE_USER_DOMAINS", parse_env = split_comma_list)]
380
380
+
pub available_user_domains: Option<Vec<String>>,
381
381
+
382
382
+
/// Enable PDS-hosted did:web identities. Hosting did:web requires a
383
383
+
/// long-term commitment to serve DID documents; opt-in only.
384
384
+
#[config(env = "ENABLE_PDS_HOSTED_DID_WEB", default = false)]
385
385
+
pub enable_pds_hosted_did_web: bool,
386
386
+
387
387
+
/// When set to true, skip age-assurance birthday prompt for all accounts.
388
388
+
#[config(env = "PDS_AGE_ASSURANCE_OVERRIDE", default = false)]
389
389
+
pub age_assurance_override: bool,
390
390
+
391
391
+
/// Require an invite code for new account registration.
392
392
+
#[config(env = "INVITE_CODE_REQUIRED", default = true)]
393
393
+
pub invite_code_required: bool,
394
394
+
395
395
+
/// Allow HTTP (non-TLS) proxy requests. Only useful during development.
396
396
+
#[config(env = "ALLOW_HTTP_PROXY", default = false)]
397
397
+
pub allow_http_proxy: bool,
398
398
+
399
399
+
/// Disable all rate limiting. Should only be used in testing.
400
400
+
#[config(env = "DISABLE_RATE_LIMITING", default = false)]
401
401
+
pub disable_rate_limiting: bool,
402
402
+
403
403
+
/// List of additional banned words for handle validation.
404
404
+
#[config(env = "PDS_BANNED_WORDS", parse_env = split_comma_list)]
405
405
+
pub banned_words: Option<Vec<String>>,
406
406
+
407
407
+
/// URL to a privacy policy page.
408
408
+
#[config(env = "PRIVACY_POLICY_URL")]
409
409
+
pub privacy_policy_url: Option<String>,
410
410
+
411
411
+
/// URL to terms of service page.
412
412
+
#[config(env = "TERMS_OF_SERVICE_URL")]
413
413
+
pub terms_of_service_url: Option<String>,
414
414
+
415
415
+
/// Operator contact email address.
416
416
+
#[config(env = "CONTACT_EMAIL")]
417
417
+
pub contact_email: Option<String>,
418
418
+
419
419
+
/// Maximum allowed blob size in bytes (default 10 GiB).
420
420
+
#[config(env = "MAX_BLOB_SIZE", default = 10_737_418_240u64)]
421
421
+
pub max_blob_size: u64,
422
422
+
}
423
423
+
424
424
+
impl ServerConfig {
425
425
+
/// The public HTTPS URL for this PDS.
426
426
+
pub fn public_url(&self) -> String {
427
427
+
format!("https://{}", self.hostname)
428
428
+
}
429
429
+
430
430
+
/// Hostname without port suffix (e.g. `pds.example.com` from
431
431
+
/// `pds.example.com:443`).
432
432
+
pub fn hostname_without_port(&self) -> &str {
433
433
+
self.hostname.split(':').next().unwrap_or(&self.hostname)
434
434
+
}
435
435
+
436
436
+
/// Returns the extra banned words list, or an empty vec when unset.
437
437
+
pub fn banned_word_list(&self) -> Vec<String> {
438
438
+
self.banned_words.clone().unwrap_or_default()
439
439
+
}
440
440
+
441
441
+
/// Returns the available user domains, falling back to `[hostname_without_port]`.
442
442
+
pub fn available_user_domain_list(&self) -> Vec<String> {
443
443
+
self.available_user_domains
444
444
+
.clone()
445
445
+
.unwrap_or_else(|| vec![self.hostname_without_port().to_string()])
446
446
+
}
447
447
+
448
448
+
/// Returns the user handle domains, falling back to `[hostname_without_port]`.
449
449
+
pub fn user_handle_domain_list(&self) -> Vec<String> {
450
450
+
self.user_handle_domains
451
451
+
.clone()
452
452
+
.unwrap_or_else(|| vec![self.hostname_without_port().to_string()])
453
453
+
}
454
454
+
}
455
455
+
456
456
+
#[derive(Debug, Config)]
457
457
+
pub struct DatabaseConfig {
458
458
+
/// PostgreSQL connection URL.
459
459
+
#[config(env = "DATABASE_URL")]
460
460
+
pub url: String,
461
461
+
462
462
+
/// Maximum number of connections in the pool.
463
463
+
#[config(env = "DATABASE_MAX_CONNECTIONS", default = 100)]
464
464
+
pub max_connections: u32,
465
465
+
466
466
+
/// Minimum number of idle connections kept in the pool.
467
467
+
#[config(env = "DATABASE_MIN_CONNECTIONS", default = 10)]
468
468
+
pub min_connections: u32,
469
469
+
470
470
+
/// Timeout in seconds when acquiring a connection from the pool.
471
471
+
#[config(env = "DATABASE_ACQUIRE_TIMEOUT_SECS", default = 10)]
472
472
+
pub acquire_timeout_secs: u64,
473
473
+
}
474
474
+
475
475
+
#[derive(Config)]
476
476
+
pub struct SecretsConfig {
477
477
+
/// Secret used for signing JWTs. Must be at least 32 characters in
478
478
+
/// production.
479
479
+
#[config(env = "JWT_SECRET")]
480
480
+
pub jwt_secret: Option<String>,
481
481
+
482
482
+
/// Secret used for DPoP proof validation. Must be at least 32 characters
483
483
+
/// in production.
484
484
+
#[config(env = "DPOP_SECRET")]
485
485
+
pub dpop_secret: Option<String>,
486
486
+
487
487
+
/// Master key used for key-encryption and HKDF derivation. Must be at
488
488
+
/// least 32 characters in production.
489
489
+
#[config(env = "MASTER_KEY")]
490
490
+
pub master_key: Option<String>,
491
491
+
492
492
+
/// PLC rotation key (DID key). If not set, user-level keys are used.
493
493
+
#[config(env = "PLC_ROTATION_KEY")]
494
494
+
pub plc_rotation_key: Option<String>,
495
495
+
496
496
+
/// Allow insecure/test secrets. NEVER enable in production.
497
497
+
#[config(env = "TRANQUIL_PDS_ALLOW_INSECURE_SECRETS", default = false)]
498
498
+
pub allow_insecure: bool,
499
499
+
}
500
500
+
501
501
+
impl std::fmt::Debug for SecretsConfig {
502
502
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503
503
+
f.debug_struct("SecretsConfig")
504
504
+
.field(
505
505
+
"jwt_secret",
506
506
+
&self.jwt_secret.as_ref().map(|_| "[REDACTED]"),
507
507
+
)
508
508
+
.field(
509
509
+
"dpop_secret",
510
510
+
&self.dpop_secret.as_ref().map(|_| "[REDACTED]"),
511
511
+
)
512
512
+
.field(
513
513
+
"master_key",
514
514
+
&self.master_key.as_ref().map(|_| "[REDACTED]"),
515
515
+
)
516
516
+
.field(
517
517
+
"plc_rotation_key",
518
518
+
&self.plc_rotation_key.as_ref().map(|_| "[REDACTED]"),
519
519
+
)
520
520
+
.field("allow_insecure", &self.allow_insecure)
521
521
+
.finish()
522
522
+
}
523
523
+
}
524
524
+
525
525
+
impl SecretsConfig {
526
526
+
/// Resolve the JWT secret, falling back to an insecure default if
527
527
+
/// `allow_insecure` is true.
528
528
+
pub fn jwt_secret_or_default(&self) -> String {
529
529
+
self.jwt_secret.clone().unwrap_or_else(|| {
530
530
+
if cfg!(test) || self.allow_insecure {
531
531
+
"test-jwt-secret-not-for-production".to_string()
532
532
+
} else {
533
533
+
panic!(
534
534
+
"JWT_SECRET must be set in production. \
535
535
+
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
536
536
+
);
537
537
+
}
538
538
+
})
539
539
+
}
540
540
+
541
541
+
/// Resolve the DPoP secret, falling back to an insecure default if
542
542
+
/// `allow_insecure` is true.
543
543
+
pub fn dpop_secret_or_default(&self) -> String {
544
544
+
self.dpop_secret.clone().unwrap_or_else(|| {
545
545
+
if cfg!(test) || self.allow_insecure {
546
546
+
"test-dpop-secret-not-for-production".to_string()
547
547
+
} else {
548
548
+
panic!(
549
549
+
"DPOP_SECRET must be set in production. \
550
550
+
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
551
551
+
);
552
552
+
}
553
553
+
})
554
554
+
}
555
555
+
556
556
+
/// Resolve the master key, falling back to an insecure default if
557
557
+
/// `allow_insecure` is true.
558
558
+
pub fn master_key_or_default(&self) -> String {
559
559
+
self.master_key.clone().unwrap_or_else(|| {
560
560
+
if cfg!(test) || self.allow_insecure {
561
561
+
"test-master-key-not-for-production".to_string()
562
562
+
} else {
563
563
+
panic!(
564
564
+
"MASTER_KEY must be set in production. \
565
565
+
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=true for development/testing."
566
566
+
);
567
567
+
}
568
568
+
})
569
569
+
}
570
570
+
}
571
571
+
572
572
+
#[derive(Debug, Config)]
573
573
+
pub struct StorageConfig {
574
574
+
/// Storage backend: `filesystem` or `s3`.
575
575
+
#[config(env = "BLOB_STORAGE_BACKEND", default = "filesystem")]
576
576
+
pub backend: String,
577
577
+
578
578
+
/// Path on disk for the filesystem blob backend.
579
579
+
#[config(env = "BLOB_STORAGE_PATH", default = "/var/lib/tranquil-pds/blobs")]
580
580
+
pub path: String,
581
581
+
582
582
+
/// S3 bucket name for blob storage.
583
583
+
#[config(env = "S3_BUCKET")]
584
584
+
pub s3_bucket: Option<String>,
585
585
+
586
586
+
/// Custom S3 endpoint URL (for MinIO, R2, etc.).
587
587
+
#[config(env = "S3_ENDPOINT")]
588
588
+
pub s3_endpoint: Option<String>,
589
589
+
}
590
590
+
591
591
+
#[derive(Debug, Config)]
592
592
+
pub struct BackupConfig {
593
593
+
/// Enable automatic backups.
594
594
+
#[config(env = "BACKUP_ENABLED", default = true)]
595
595
+
pub enabled: bool,
596
596
+
597
597
+
/// Backup storage backend: `filesystem` or `s3`.
598
598
+
#[config(env = "BACKUP_STORAGE_BACKEND", default = "filesystem")]
599
599
+
pub backend: String,
600
600
+
601
601
+
/// Path on disk for the filesystem backup backend.
602
602
+
#[config(env = "BACKUP_STORAGE_PATH", default = "/var/lib/tranquil-pds/backups")]
603
603
+
pub path: String,
604
604
+
605
605
+
/// S3 bucket name for backups.
606
606
+
#[config(env = "BACKUP_S3_BUCKET")]
607
607
+
pub s3_bucket: Option<String>,
608
608
+
609
609
+
/// Number of backup revisions to keep per account.
610
610
+
#[config(env = "BACKUP_RETENTION_COUNT", default = 7)]
611
611
+
pub retention_count: u32,
612
612
+
613
613
+
/// Seconds between backup runs.
614
614
+
#[config(env = "BACKUP_INTERVAL_SECS", default = 86400)]
615
615
+
pub interval_secs: u64,
616
616
+
}
617
617
+
618
618
+
#[derive(Debug, Config)]
619
619
+
pub struct CacheConfig {
620
620
+
/// Cache backend: `ripple` (default, built-in gossip) or `valkey`.
621
621
+
#[config(env = "CACHE_BACKEND", default = "ripple")]
622
622
+
pub backend: String,
623
623
+
624
624
+
/// Valkey / Redis connection URL. Required when `backend = "valkey"`.
625
625
+
#[config(env = "VALKEY_URL")]
626
626
+
pub valkey_url: Option<String>,
627
627
+
628
628
+
#[config(nested)]
629
629
+
pub ripple: RippleCacheConfig,
630
630
+
}
631
631
+
632
632
+
#[derive(Debug, Config)]
633
633
+
pub struct PlcConfig {
634
634
+
/// Base URL of the PLC directory.
635
635
+
#[config(env = "PLC_DIRECTORY_URL", default = "https://plc.directory")]
636
636
+
pub directory_url: String,
637
637
+
638
638
+
/// HTTP request timeout in seconds.
639
639
+
#[config(env = "PLC_TIMEOUT_SECS", default = 10)]
640
640
+
pub timeout_secs: u64,
641
641
+
642
642
+
/// TCP connect timeout in seconds.
643
643
+
#[config(env = "PLC_CONNECT_TIMEOUT_SECS", default = 5)]
644
644
+
pub connect_timeout_secs: u64,
645
645
+
646
646
+
/// Seconds to cache DID documents in memory.
647
647
+
#[config(env = "DID_CACHE_TTL_SECS", default = 300)]
648
648
+
pub did_cache_ttl_secs: u64,
649
649
+
}
650
650
+
651
651
+
#[derive(Debug, Config)]
652
652
+
pub struct FirehoseConfig {
653
653
+
/// Size of the in-memory broadcast buffer for firehose events.
654
654
+
#[config(env = "FIREHOSE_BUFFER_SIZE", default = 10000)]
655
655
+
pub buffer_size: usize,
656
656
+
657
657
+
/// How many hours of historical events to replay for cursor-based
658
658
+
/// firehose connections.
659
659
+
#[config(env = "FIREHOSE_BACKFILL_HOURS", default = 72)]
660
660
+
pub backfill_hours: i64,
661
661
+
662
662
+
/// Maximum number of lagged events before disconnecting a slow consumer.
663
663
+
#[config(env = "FIREHOSE_MAX_LAG", default = 5000)]
664
664
+
pub max_lag: u64,
665
665
+
666
666
+
/// List of relay / crawler notification URLs.
667
667
+
#[config(env = "CRAWLERS", parse_env = split_comma_list)]
668
668
+
pub crawlers: Option<Vec<String>>,
669
669
+
}
670
670
+
671
671
+
impl FirehoseConfig {
672
672
+
/// Returns the list of crawler URLs, falling back to `["https://bsky.network"]`
673
673
+
/// when none are configured.
674
674
+
pub fn crawler_list(&self) -> Vec<String> {
675
675
+
self.crawlers
676
676
+
.clone()
677
677
+
.unwrap_or_else(|| vec!["https://bsky.network".to_string()])
678
678
+
}
679
679
+
}
680
680
+
681
681
+
#[derive(Debug, Config)]
682
682
+
pub struct EmailConfig {
683
683
+
/// Sender email address. When unset, email sending is disabled.
684
684
+
#[config(env = "MAIL_FROM_ADDRESS")]
685
685
+
pub from_address: Option<String>,
686
686
+
687
687
+
/// Display name used in the `From` header.
688
688
+
#[config(env = "MAIL_FROM_NAME", default = "Tranquil PDS")]
689
689
+
pub from_name: String,
690
690
+
691
691
+
/// Path to the `sendmail` binary.
692
692
+
#[config(env = "SENDMAIL_PATH", default = "/usr/sbin/sendmail")]
693
693
+
pub sendmail_path: String,
694
694
+
}
695
695
+
696
696
+
#[derive(Debug, Config)]
697
697
+
pub struct DiscordConfig {
698
698
+
/// Discord bot token. When unset, Discord integration is disabled.
699
699
+
#[config(env = "DISCORD_BOT_TOKEN")]
700
700
+
pub bot_token: Option<String>,
701
701
+
}
702
702
+
703
703
+
#[derive(Debug, Config)]
704
704
+
pub struct TelegramConfig {
705
705
+
/// Telegram bot token. When unset, Telegram integration is disabled.
706
706
+
#[config(env = "TELEGRAM_BOT_TOKEN")]
707
707
+
pub bot_token: Option<String>,
708
708
+
709
709
+
/// Secret token for incoming webhook verification.
710
710
+
#[config(env = "TELEGRAM_WEBHOOK_SECRET")]
711
711
+
pub webhook_secret: Option<String>,
712
712
+
}
713
713
+
714
714
+
#[derive(Debug, Config)]
715
715
+
pub struct SignalConfig {
716
716
+
/// Path to the `signal-cli` binary.
717
717
+
#[config(env = "SIGNAL_CLI_PATH", default = "/usr/local/bin/signal-cli")]
718
718
+
pub cli_path: String,
719
719
+
720
720
+
/// Sender phone number. When unset, Signal integration is disabled.
721
721
+
#[config(env = "SIGNAL_SENDER_NUMBER")]
722
722
+
pub sender_number: Option<String>,
723
723
+
}
724
724
+
725
725
+
#[derive(Debug, Config)]
726
726
+
pub struct NotificationConfig {
727
727
+
/// Polling interval in milliseconds for the comms queue.
728
728
+
#[config(env = "NOTIFICATION_POLL_INTERVAL_MS", default = 1000)]
729
729
+
pub poll_interval_ms: u64,
730
730
+
731
731
+
/// Number of notifications to process per batch.
732
732
+
#[config(env = "NOTIFICATION_BATCH_SIZE", default = 100)]
733
733
+
pub batch_size: i64,
734
734
+
}
735
735
+
736
736
+
#[derive(Debug, Config)]
737
737
+
pub struct SsoConfig {
738
738
+
#[config(nested)]
739
739
+
pub github: SsoProviderConfig,
740
740
+
741
741
+
#[config(nested)]
742
742
+
pub discord: SsoDiscordProviderConfig,
743
743
+
744
744
+
#[config(nested)]
745
745
+
pub google: SsoProviderConfig,
746
746
+
747
747
+
#[config(nested)]
748
748
+
pub gitlab: SsoProviderWithIssuerConfig,
749
749
+
750
750
+
#[config(nested)]
751
751
+
pub oidc: SsoProviderWithIssuerConfig,
752
752
+
753
753
+
#[config(nested)]
754
754
+
pub apple: SsoAppleConfig,
755
755
+
}
756
756
+
757
757
+
// Generic SSO provider (GitHub, Google)
758
758
+
#[derive(Debug, Config)]
759
759
+
pub struct SsoProviderConfig {
760
760
+
#[config(default = false)]
761
761
+
pub enabled: bool,
762
762
+
pub client_id: Option<String>,
763
763
+
pub client_secret: Option<String>,
764
764
+
pub display_name: Option<String>,
765
765
+
}
766
766
+
767
767
+
// SSO provider with custom env prefixes for Discord
768
768
+
// (since the nested TOML key is `sso.discord` but env vars are `SSO_DISCORD_*`)
769
769
+
#[derive(Debug, Config)]
770
770
+
pub struct SsoDiscordProviderConfig {
771
771
+
#[config(default = false)]
772
772
+
pub enabled: bool,
773
773
+
pub client_id: Option<String>,
774
774
+
pub client_secret: Option<String>,
775
775
+
pub display_name: Option<String>,
776
776
+
}
777
777
+
778
778
+
// SSO providers that require an issuer URL (GitLab, OIDC)
779
779
+
#[derive(Debug, Config)]
780
780
+
pub struct SsoProviderWithIssuerConfig {
781
781
+
#[config(default = false)]
782
782
+
pub enabled: bool,
783
783
+
pub client_id: Option<String>,
784
784
+
pub client_secret: Option<String>,
785
785
+
pub issuer: Option<String>,
786
786
+
pub display_name: Option<String>,
787
787
+
}
788
788
+
789
789
+
#[derive(Debug, Config)]
790
790
+
pub struct SsoAppleConfig {
791
791
+
#[config(env = "SSO_APPLE_ENABLED", default = false)]
792
792
+
pub enabled: bool,
793
793
+
794
794
+
#[config(env = "SSO_APPLE_CLIENT_ID")]
795
795
+
pub client_id: Option<String>,
796
796
+
797
797
+
#[config(env = "SSO_APPLE_TEAM_ID")]
798
798
+
pub team_id: Option<String>,
799
799
+
800
800
+
#[config(env = "SSO_APPLE_KEY_ID")]
801
801
+
pub key_id: Option<String>,
802
802
+
803
803
+
#[config(env = "SSO_APPLE_PRIVATE_KEY")]
804
804
+
pub private_key: Option<String>,
805
805
+
}
806
806
+
807
807
+
#[derive(Debug, Config)]
808
808
+
pub struct ModerationConfig {
809
809
+
/// External report-handling service URL.
810
810
+
#[config(env = "REPORT_SERVICE_URL")]
811
811
+
pub report_service_url: Option<String>,
812
812
+
813
813
+
/// DID of the external report-handling service.
814
814
+
#[config(env = "REPORT_SERVICE_DID")]
815
815
+
pub report_service_did: Option<String>,
816
816
+
}
817
817
+
818
818
+
#[derive(Debug, Config)]
819
819
+
pub struct ImportConfig {
820
820
+
/// Whether the PDS accepts repo imports.
821
821
+
#[config(env = "ACCEPTING_REPO_IMPORTS", default = true)]
822
822
+
pub accepting: bool,
823
823
+
824
824
+
/// Maximum allowed import archive size in bytes (default 1 GiB).
825
825
+
#[config(env = "MAX_IMPORT_SIZE", default = 1_073_741_824)]
826
826
+
pub max_size: u64,
827
827
+
828
828
+
/// Maximum number of blocks allowed in an import.
829
829
+
#[config(env = "MAX_IMPORT_BLOCKS", default = 500000)]
830
830
+
pub max_blocks: u64,
831
831
+
832
832
+
/// Skip CAR verification during import. Only for development/debugging.
833
833
+
#[config(env = "SKIP_IMPORT_VERIFICATION", default = false)]
834
834
+
pub skip_verification: bool,
835
835
+
}
836
836
+
837
837
+
/// Parse a comma-separated environment variable into a `Vec<String>`,
838
838
+
/// trimming whitespace and dropping empty entries.
839
839
+
///
840
840
+
/// Signature matches confique's `parse_env` expectation: `fn(&str) -> Result<T, E>`.
841
841
+
fn split_comma_list(value: &str) -> Result<Vec<String>, std::convert::Infallible> {
842
842
+
Ok(value
843
843
+
.split(',')
844
844
+
.map(|item| item.trim().to_string())
845
845
+
.filter(|item| !item.is_empty())
846
846
+
.collect())
847
847
+
}
848
848
+
849
849
+
#[derive(Debug, Config)]
850
850
+
pub struct RippleCacheConfig {
851
851
+
/// Address to bind the Ripple gossip protocol listener.
852
852
+
#[config(env = "RIPPLE_BIND", default = "0.0.0.0:0")]
853
853
+
pub bind_addr: String,
854
854
+
855
855
+
/// List of seed peer addresses.
856
856
+
#[config(env = "RIPPLE_PEERS", parse_env = split_comma_list)]
857
857
+
pub peers: Option<Vec<String>>,
858
858
+
859
859
+
/// Unique machine identifier. Auto-derived from hostname when not set.
860
860
+
#[config(env = "RIPPLE_MACHINE_ID")]
861
861
+
pub machine_id: Option<u64>,
862
862
+
863
863
+
/// Gossip protocol interval in milliseconds.
864
864
+
#[config(env = "RIPPLE_GOSSIP_INTERVAL_MS", default = 200)]
865
865
+
pub gossip_interval_ms: u64,
866
866
+
867
867
+
/// Maximum cache size in megabytes.
868
868
+
#[config(env = "RIPPLE_CACHE_MAX_MB", default = 256)]
869
869
+
pub cache_max_mb: usize,
870
870
+
}
871
871
+
872
872
+
#[derive(Debug, Config)]
873
873
+
pub struct ScheduledConfig {
874
874
+
/// Interval in seconds between scheduled delete checks.
875
875
+
#[config(env = "SCHEDULED_DELETE_CHECK_INTERVAL_SECS", default = 3600)]
876
876
+
pub delete_check_interval_secs: u64,
877
877
+
}
878
878
+
879
879
+
/// Generate a TOML configuration template with all available options,
880
880
+
/// defaults, and documentation comments.
881
881
+
pub fn template() -> String {
882
882
+
confique::toml::template::<TranquilConfig>(confique::toml::FormatOptions::default())
883
883
+
}
···
5
5
license.workspace = true
6
6
7
7
[dependencies]
8
8
+
tranquil-config = { workspace = true }
9
9
+
8
10
async-trait = { workspace = true }
9
11
bytes = { workspace = true }
10
12
futures = { workspace = true }
···
45
45
}
46
46
47
47
pub fn backup_retention_count() -> u32 {
48
48
-
std::env::var("BACKUP_RETENTION_COUNT")
49
49
-
.ok()
50
50
-
.and_then(|v| v.parse().ok())
48
48
+
tranquil_config::try_get()
49
49
+
.map(|c| c.backup.retention_count)
51
50
.unwrap_or(7)
52
51
}
53
52
54
53
pub fn backup_interval_secs() -> u64 {
55
55
-
std::env::var("BACKUP_INTERVAL_SECS")
56
56
-
.ok()
57
57
-
.and_then(|v| v.parse().ok())
54
54
+
tranquil_config::try_get()
55
55
+
.map(|c| c.backup.interval_secs)
58
56
.unwrap_or(86400)
59
57
}
60
58
···
6
6
7
7
[dependencies]
8
8
tranquil-types = { workspace = true }
9
9
+
tranquil-config = { workspace = true }
9
10
tranquil-crypto = { workspace = true }
10
11
tranquil-storage = { workspace = true }
11
12
tranquil-cache = { workspace = true }
···
29
30
bytes = { workspace = true }
30
31
chrono = { workspace = true }
31
32
cid = { workspace = true }
33
33
+
clap = { workspace = true }
32
34
dotenvy = { workspace = true }
33
35
ed25519-dalek = { workspace = true }
34
36
futures = { workspace = true }
···
2
2
use crate::auth::{Admin, Auth};
3
3
use crate::state::AppState;
4
4
use crate::types::Did;
5
5
-
use crate::util::pds_hostname;
6
5
use axum::{
7
6
Json,
8
7
extract::State,
···
45
44
46
45
let email = user.email.ok_or(ApiError::NoEmail)?;
47
46
let (user_id, handle) = (user.id, user.handle);
48
48
-
let hostname = pds_hostname();
47
47
+
let hostname = &tranquil_config::get().server.hostname;
49
48
let subject = input
50
49
.subject
51
50
.clone()
···
3
3
use crate::auth::{Admin, Auth};
4
4
use crate::state::AppState;
5
5
use crate::types::{Did, Handle, PlainPassword};
6
6
-
use crate::util::pds_hostname_without_port;
7
6
use axum::{
8
7
Json,
9
8
extract::State,
···
70
69
{
71
70
return Err(ApiError::InvalidHandle(None));
72
71
}
73
73
-
let hostname_for_handles = pds_hostname_without_port();
72
72
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
74
73
let handle = if !input_handle.contains('.') {
75
74
format!("{}.{}", input_handle, hostname_for_handles)
76
75
} else {
···
8
8
use crate::rate_limit::{AccountCreationLimit, RateLimited};
9
9
use crate::state::AppState;
10
10
use crate::types::{Did, Handle};
11
11
-
use crate::util::{pds_hostname, pds_hostname_without_port};
12
11
use axum::{
13
12
Json,
14
13
extract::{Query, State},
···
435
434
Err(response) => return Ok(response),
436
435
};
437
436
438
438
-
let hostname = pds_hostname();
439
439
-
let hostname_for_handles = pds_hostname_without_port();
437
437
+
let hostname = &tranquil_config::get().server.hostname;
438
438
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
440
439
let pds_suffix = format!(".{}", hostname_for_handles);
441
440
442
441
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
···
475
474
Err(_) => return Ok(ApiError::InvalidInviteCode.into_response()),
476
475
}
477
476
} else {
478
478
-
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
477
477
+
let invite_required = tranquil_config::get().server.invite_code_required;
479
478
if invite_required {
480
479
return Ok(ApiError::InviteCodeRequired.into_response());
481
480
}
···
497
496
}
498
497
};
499
498
500
500
-
let rotation_key = std::env::var("PLC_ROTATION_KEY")
501
501
-
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
499
499
+
let rotation_key = tranquil_config::get()
500
500
+
.secrets
501
501
+
.plc_rotation_key
502
502
+
.clone()
503
503
+
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
502
504
503
505
let genesis_result = match crate::plc::create_genesis_operation(
504
506
&signing_key,
···
12
12
13
13
use crate::comms::comms_repo;
14
14
use crate::state::AppState;
15
15
-
use crate::util::{discord_public_key, pds_hostname};
15
15
+
use crate::util::discord_public_key;
16
16
17
17
#[derive(Deserialize)]
18
18
struct Interaction {
···
185
185
user_id,
186
186
tranquil_db_traits::CommsChannel::Discord,
187
187
&discord_user_id,
188
188
-
pds_hostname(),
188
188
+
&tranquil_config::get().server.hostname,
189
189
)
190
190
.await
191
191
{
···
6
6
use crate::rate_limit::{AccountCreationLimit, RateLimited};
7
7
use crate::state::AppState;
8
8
use crate::types::{Did, Handle, PlainPassword};
9
9
-
use crate::util::{pds_hostname, pds_hostname_without_port};
10
9
use crate::validation::validate_password;
11
10
use axum::{
12
11
Json,
···
141
140
}
142
141
}
143
142
144
144
-
let hostname_for_validation = pds_hostname_without_port();
143
143
+
let hostname_for_validation = tranquil_config::get().server.hostname_without_port();
145
144
let pds_suffix = format!(".{}", hostname_for_validation);
146
145
147
146
let validated_short_handle = if !input.handle.contains('.')
···
233
232
},
234
233
})
235
234
};
236
236
-
let hostname = pds_hostname();
237
237
-
let hostname_for_handles = pds_hostname_without_port();
235
235
+
let hostname = &tranquil_config::get().server.hostname;
236
236
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
238
237
let pds_endpoint = format!("https://{}", hostname);
239
238
let suffix = format!(".{}", hostname_for_handles);
240
239
let handle = if input.handle.ends_with(&suffix) {
···
326
325
)
327
326
.into_response();
328
327
} else {
329
329
-
let rotation_key = std::env::var("PLC_ROTATION_KEY")
330
330
-
.unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
328
328
+
let rotation_key = tranquil_config::get()
329
329
+
.secrets
330
330
+
.plc_rotation_key
331
331
+
.clone()
332
332
+
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
331
333
let genesis_result = match create_genesis_operation(
332
334
&signing_key,
333
335
&rotation_key,
···
359
361
genesis_result.did
360
362
}
361
363
} else {
362
362
-
let rotation_key = std::env::var("PLC_ROTATION_KEY")
363
363
-
.unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
364
364
+
let rotation_key = tranquil_config::get()
365
365
+
.secrets
366
366
+
.plc_rotation_key
367
367
+
.clone()
368
368
+
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
364
369
let genesis_result = match create_genesis_operation(
365
370
&signing_key,
366
371
&rotation_key,
···
473
478
error!("Error creating session: {:?}", e);
474
479
return ApiError::InternalError(None).into_response();
475
480
}
476
476
-
let hostname = pds_hostname();
481
481
+
let hostname = &tranquil_config::get().server.hostname;
477
482
let verification_required = if let Some(ref user_email) = email {
478
483
let token = crate::auth::verification_token::generate_migration_token(
479
484
&did_typed, user_email,
···
543
548
return ApiError::HandleTaken.into_response();
544
549
}
545
550
546
546
-
let invite_code_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
551
551
+
let invite_code_required = tranquil_config::get().server.invite_code_required;
547
552
if invite_code_required
548
553
&& input
549
554
.invite_code
···
632
637
let rev_str = rev.as_ref().to_string();
633
638
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
634
639
635
635
-
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
636
636
-
json!({
640
640
+
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
641
641
+
Some(json!({
637
642
"$type": "app.bsky.actor.defs#personalDetailsPref",
638
643
"birthDate": "1998-05-06T00:00:00.000Z"
639
639
-
})
640
640
-
});
644
644
+
}))
645
645
+
} else {
646
646
+
None
647
647
+
};
641
648
642
649
let preferred_comms_channel = verification_channel;
643
650
···
748
755
warn!("Failed to create default profile for {}: {}", did, e);
749
756
}
750
757
}
751
751
-
let hostname = pds_hostname();
758
758
+
let hostname = &tranquil_config::get().server.hostname;
752
759
if !is_migration {
753
760
if let Some(ref recipient) = verification_recipient {
754
761
let verification_token = crate::auth::verification_token::generate_signup_token(
···
6
6
};
7
7
use crate::state::AppState;
8
8
use crate::types::Handle;
9
9
-
use crate::util::{get_header_str, pds_hostname, pds_hostname_without_port};
9
9
+
use crate::util::get_header_str;
10
10
use axum::{
11
11
Json,
12
12
extract::{Path, Query, State},
···
122
122
}
123
123
124
124
pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
125
125
-
let hostname = pds_hostname();
126
126
-
let hostname_without_port = pds_hostname_without_port();
125
125
+
let hostname = &tranquil_config::get().server.hostname;
126
126
+
let hostname_without_port = tranquil_config::get().server.hostname_without_port();
127
127
let host_header = get_header_str(&headers, http::header::HOST).unwrap_or(hostname);
128
128
let host_without_port = host_header.split(':').next().unwrap_or(host_header);
129
129
if host_without_port != hostname_without_port
···
275
275
}
276
276
277
277
pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
278
278
-
let hostname = pds_hostname();
279
279
-
let hostname_for_handles = pds_hostname_without_port();
278
278
+
let hostname = &tranquil_config::get().server.hostname;
279
279
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
280
280
let current_handle = format!("{}.{}", handle, hostname_for_handles);
281
281
let current_handle_typed: Handle = match current_handle.parse() {
282
282
Ok(h) => h,
···
571
571
ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into()))
572
572
})?;
573
573
574
574
-
let hostname = pds_hostname();
574
574
+
let hostname = &tranquil_config::get().server.hostname;
575
575
let pds_endpoint = format!("https://{}", hostname);
576
576
let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)
577
577
.map_err(|_| ApiError::InternalError(None))?;
···
579
579
let rotation_keys = if auth.did.starts_with("did:web:") {
580
580
vec![]
581
581
} else {
582
582
-
let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
583
583
-
Ok(key) => key,
584
584
-
Err(_) => {
582
582
+
let server_rotation_key = match &tranquil_config::get().secrets.plc_rotation_key {
583
583
+
Some(key) => key.clone(),
584
584
+
None => {
585
585
warn!(
586
586
"PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"
587
587
);
···
675
675
"Inappropriate language in handle".into(),
676
676
)));
677
677
}
678
678
-
let hostname_for_handles = pds_hostname_without_port();
678
678
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
679
679
let suffix = format!(".{}", hostname_for_handles);
680
680
let is_service_domain =
681
681
crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles);
···
2
2
use crate::api::error::{ApiError, DbResultExt};
3
3
use crate::auth::{Auth, Permissive};
4
4
use crate::state::AppState;
5
5
-
use crate::util::pds_hostname;
6
5
use axum::{
7
6
extract::State,
8
7
response::{IntoResponse, Response},
···
41
40
.await
42
41
.log_db_err("creating PLC token")?;
43
42
44
44
-
let hostname = pds_hostname();
43
43
+
let hostname = &tranquil_config::get().server.hostname;
45
44
if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation(
46
45
state.user_repo.as_ref(),
47
46
state.infra_repo.as_ref(),
···
4
4
use crate::circuit_breaker::with_circuit_breaker;
5
5
use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation};
6
6
use crate::state::AppState;
7
7
-
use crate::util::pds_hostname;
8
7
use axum::{
9
8
Json,
10
9
extract::State,
···
42
41
.map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?;
43
42
44
43
let op = &input.operation;
45
45
-
let hostname = pds_hostname();
44
44
+
let hostname = &tranquil_config::get().server.hostname;
46
45
let public_url = format!("https://{}", hostname);
47
46
let user = state
48
47
.user_repo
···
70
69
})?;
71
70
72
71
let user_did_key = signing_key_to_did_key(&signing_key);
73
73
-
let server_rotation_key =
74
74
-
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
72
72
+
let server_rotation_key = tranquil_config::get()
73
73
+
.secrets
74
74
+
.plc_rotation_key
75
75
+
.clone()
76
76
+
.unwrap_or_else(|| user_did_key.clone());
75
77
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
76
78
let has_server_key = rotation_keys
77
79
.iter()
···
69
69
}
70
70
71
71
fn get_report_service_config() -> Option<ReportServiceConfig> {
72
72
-
let url = std::env::var("REPORT_SERVICE_URL").ok()?;
73
73
-
let did = std::env::var("REPORT_SERVICE_DID").ok()?;
72
72
+
let cfg = tranquil_config::get();
73
73
+
let url = cfg.moderation.report_service_url.clone()?;
74
74
+
let did = cfg.moderation.report_service_did.clone()?;
74
75
if url.is_empty() || did.is_empty() {
75
76
return None;
76
77
}
···
1
1
use crate::api::error::ApiError;
2
2
use crate::auth::{Active, Auth};
3
3
use crate::state::AppState;
4
4
-
use crate::util::pds_hostname;
5
4
use axum::{
6
5
Json,
7
6
extract::State,
···
148
147
149
148
match channel {
150
149
CommsChannel::Email => {
151
151
-
let hostname = pds_hostname();
150
150
+
let hostname = &tranquil_config::get().server.hostname;
152
151
let handle_str = handle.unwrap_or("user");
153
152
crate::comms::comms_repo::enqueue_email_update(
154
153
state.infra_repo.as_ref(),
···
167
166
})?;
168
167
}
169
168
_ => {
170
170
-
let hostname = pds_hostname();
169
169
+
let hostname = &tranquil_config::get().server.hostname;
171
170
let encoded_token = urlencoding::encode(&formatted_token);
172
171
let encoded_identifier = urlencoding::encode(identifier);
173
172
let verify_link = format!(
···
168
168
}
169
169
170
170
// 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
171
171
-
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok()
171
171
+
if tranquil_config::get().server.age_assurance_override
172
172
&& (path.ends_with("app.bsky.ageassurance.getState")
173
173
|| path.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
174
174
{
···
63
63
let parsed = Url::parse(url).map_err(|_| SsrfError::InvalidUrl)?;
64
64
let scheme = parsed.scheme();
65
65
if scheme != "https" {
66
66
-
let allow_http = std::env::var("ALLOW_HTTP_PROXY").is_ok()
66
66
+
let allow_http = tranquil_config::get().server.allow_http_proxy
67
67
|| url.starts_with("http://127.0.0.1")
68
68
|| url.starts_with("http://localhost");
69
69
if !allow_http {
···
3
3
use crate::delegation::DelegationActionType;
4
4
use crate::state::AppState;
5
5
use crate::types::{CidLink, Did};
6
6
-
use crate::util::{get_header_str, get_max_blob_size};
6
6
+
use crate::util::get_header_str;
7
7
use axum::body::Body;
8
8
use axum::{
9
9
Json,
···
89
89
.ok_or(ApiError::InternalError(None))?;
90
90
91
91
let temp_key = format!("temp/{}", uuid::Uuid::new_v4());
92
92
-
let max_size = u64::try_from(get_max_blob_size()).unwrap_or(u64::MAX);
92
92
+
let max_size = tranquil_config::get().server.max_blob_size;
93
93
94
94
let body_stream = body.into_data_stream();
95
95
let mapped_stream =
···
18
18
use tracing::{debug, error, info, warn};
19
19
use tranquil_types::{AtUri, CidLink};
20
20
21
21
-
const DEFAULT_MAX_IMPORT_SIZE: usize = 1024 * 1024 * 1024;
22
22
-
const DEFAULT_MAX_BLOCKS: usize = 500000;
23
23
-
24
21
pub async fn import_repo(
25
22
State(state): State<AppState>,
26
23
auth: Auth<NotTakendown>,
27
24
body: Bytes,
28
25
) -> Result<Response, ApiError> {
29
29
-
let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS")
30
30
-
.map(|v| v != "false" && v != "0")
31
31
-
.unwrap_or(true);
26
26
+
let accepting_imports = tranquil_config::get().import.accepting;
32
27
if !accepting_imports {
33
28
return Err(ApiError::InvalidRequest(
34
29
"Service is not accepting repo imports".into(),
35
30
));
36
31
}
37
37
-
let max_size: usize = std::env::var("MAX_IMPORT_SIZE")
38
38
-
.ok()
39
39
-
.and_then(|s| s.parse().ok())
40
40
-
.unwrap_or(DEFAULT_MAX_IMPORT_SIZE);
32
32
+
let max_size = tranquil_config::get().import.max_size as usize;
41
33
if body.len() > max_size {
42
34
return Err(ApiError::PayloadTooLarge(format!(
43
35
"Import size exceeds limit of {} bytes",
···
108
100
commit_did, did
109
101
)));
110
102
}
111
111
-
let skip_verification = crate::util::parse_env_bool("SKIP_IMPORT_VERIFICATION");
103
103
+
let skip_verification = tranquil_config::get().import.skip_verification;
112
104
let is_migration = user.deactivated_at.is_some();
113
105
if skip_verification {
114
106
warn!("Skipping all CAR verification for import (SKIP_IMPORT_VERIFICATION=true)");
···
196
188
}
197
189
}
198
190
}
199
199
-
let max_blocks: usize = std::env::var("MAX_IMPORT_BLOCKS")
200
200
-
.ok()
201
201
-
.and_then(|s| s.parse().ok())
202
202
-
.unwrap_or(DEFAULT_MAX_BLOCKS);
191
191
+
let max_blocks = tranquil_config::get().import.max_blocks as usize;
203
192
let _write_lock = state.repo_write_locks.lock(user_id).await;
204
193
match apply_import(
205
194
&state.repo_repo,
···
324
313
{
325
314
warn!("Failed to sequence import event: {:?}", e);
326
315
}
327
327
-
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() {
316
316
+
if tranquil_config::get().server.age_assurance_override {
328
317
let birthdate_pref = json!({
329
318
"$type": "app.bsky.actor.defs#personalDetailsPref",
330
319
"birthDate": "1998-05-06T00:00:00.000Z"
···
1
1
use crate::api::error::ApiError;
2
2
use crate::state::AppState;
3
3
use crate::types::AtIdentifier;
4
4
-
use crate::util::pds_hostname_without_port;
5
4
use axum::{
6
5
Json,
7
6
extract::{Query, State},
···
19
18
State(state): State<AppState>,
20
19
Query(input): Query<DescribeRepoInput>,
21
20
) -> Response {
22
22
-
let hostname_for_handles = pds_hostname_without_port();
21
21
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
23
22
let user_row = if input.repo.is_did() {
24
23
let did: crate::types::Did = match input.repo.as_str().parse() {
25
24
Ok(d) => d,
···
2
2
use crate::api::error::ApiError;
3
3
use crate::state::AppState;
4
4
use crate::types::{AtIdentifier, Nsid, Rkey};
5
5
-
use crate::util::pds_hostname_without_port;
6
5
use axum::{
7
6
Json,
8
7
extract::{Query, State},
···
60
59
_headers: HeaderMap,
61
60
Query(input): Query<GetRecordInput>,
62
61
) -> Response {
63
63
-
let hostname_for_handles = pds_hostname_without_port();
62
62
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
64
63
let user_id_opt = if input.repo.is_did() {
65
64
let did: crate::types::Did = match input.repo.as_str().parse() {
66
65
Ok(d) => d,
···
159
158
State(state): State<AppState>,
160
159
Query(input): Query<ListRecordsInput>,
161
160
) -> Response {
162
162
-
let hostname_for_handles = pds_hostname_without_port();
161
161
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
163
162
let user_id_opt = if input.repo.is_did() {
164
163
let did: crate::types::Did = match input.repo.as_str().parse() {
165
164
Ok(d) => d,
···
5
5
use crate::plc::PlcClient;
6
6
use crate::state::AppState;
7
7
use crate::types::PlainPassword;
8
8
-
use crate::util::pds_hostname;
9
8
use axum::{
10
9
Json,
11
10
extract::State,
···
131
130
did: &crate::types::Did,
132
131
with_retry: bool,
133
132
) -> Result<(), ApiError> {
134
134
-
let hostname = pds_hostname();
133
133
+
let hostname = &tranquil_config::get().server.hostname;
135
134
let expected_endpoint = format!("https://{}", hostname);
136
135
137
136
if did.as_str().starts_with("did:plc:") {
···
201
200
.await
202
201
.map_err(ApiError::InvalidRequest)?;
203
202
204
204
-
let server_rotation_key = std::env::var("PLC_ROTATION_KEY").ok();
203
203
+
let server_rotation_key = tranquil_config::get().secrets.plc_rotation_key.clone();
205
204
if let Some(ref expected_rotation_key) = server_rotation_key {
206
205
let rotation_keys = doc_data
207
206
.get("rotationKeys")
···
552
551
.create_deletion_request(&confirmation_token, session_mfa.did(), expires_at)
553
552
.await
554
553
.log_db_err("creating deletion token")?;
555
555
-
let hostname = pds_hostname();
554
554
+
let hostname = &tranquil_config::get().server.hostname;
556
555
if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion(
557
556
state.user_repo.as_ref(),
558
557
state.infra_repo.as_ref(),
···
3
3
use crate::auth::{Auth, NotTakendown};
4
4
use crate::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
5
5
use crate::state::AppState;
6
6
-
use crate::util::pds_hostname;
7
6
use axum::{
8
7
Json,
9
8
extract::State,
···
105
104
}
106
105
}
107
106
108
108
-
let hostname = pds_hostname();
107
107
+
let hostname = &tranquil_config::get().server.hostname;
109
108
if let Err(e) = crate::comms::comms_repo::enqueue_short_token_email(
110
109
state.user_repo.as_ref(),
111
110
state.infra_repo.as_ref(),
···
367
366
);
368
367
let formatted_token =
369
368
crate::auth::verification_token::format_token_for_display(&verification_token);
370
370
-
let hostname = pds_hostname();
369
369
+
let hostname = &tranquil_config::get().server.hostname;
371
370
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
372
371
state.user_repo.as_ref(),
373
372
state.infra_repo.as_ref(),
···
531
530
532
531
info!(did = %did, "Email update authorized via link click");
533
532
534
534
-
let hostname = pds_hostname();
533
533
+
let hostname = &tranquil_config::get().server.hostname;
535
534
let redirect_url = format!(
536
535
"https://{}/app/verify?type=email-authorize-success",
537
536
hostname
···
3
3
use crate::auth::{Admin, Auth, NotTakendown};
4
4
use crate::state::AppState;
5
5
use crate::types::Did;
6
6
-
use crate::util::pds_hostname;
7
6
use axum::{
8
7
Json,
9
8
extract::State,
···
26
25
}
27
26
28
27
fn gen_invite_code() -> String {
29
29
-
let hostname = pds_hostname();
28
28
+
let hostname = &tranquil_config::get().server.hostname;
30
29
let hostname_prefix = hostname.replace('.', "-");
31
30
format!("{}-{}", hostname_prefix, gen_random_token())
32
31
}
···
1
1
use crate::state::AppState;
2
2
-
use crate::util::{discord_app_id, discord_bot_username, pds_hostname, telegram_bot_username};
2
2
+
use crate::util::{discord_app_id, discord_bot_username, telegram_bot_username};
3
3
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
4
4
use serde_json::json;
5
5
6
6
fn get_available_comms_channels() -> Vec<tranquil_db_traits::CommsChannel> {
7
7
use tranquil_db_traits::CommsChannel;
8
8
+
let cfg = tranquil_config::get();
8
9
let mut channels = vec![CommsChannel::Email];
9
9
-
if std::env::var("DISCORD_BOT_TOKEN").is_ok() {
10
10
+
if cfg.discord.bot_token.is_some() {
10
11
channels.push(CommsChannel::Discord);
11
12
}
12
12
-
if std::env::var("TELEGRAM_BOT_TOKEN").is_ok() {
13
13
+
if cfg.telegram.bot_token.is_some() {
13
14
channels.push(CommsChannel::Telegram);
14
15
}
15
15
-
if std::env::var("SIGNAL_CLI_PATH").is_ok() && std::env::var("SIGNAL_SENDER_NUMBER").is_ok() {
16
16
+
if cfg.signal.sender_number.is_some() {
16
17
channels.push(CommsChannel::Signal);
17
18
}
18
19
channels
···
26
27
)
27
28
}
28
29
pub fn is_self_hosted_did_web_enabled() -> bool {
29
29
-
std::env::var("ENABLE_SELF_HOSTED_DID_WEB")
30
30
-
.map(|v| v != "false" && v != "0")
31
31
-
.unwrap_or(true)
30
30
+
tranquil_config::get().server.enable_pds_hosted_did_web
32
31
}
33
32
34
33
pub async fn describe_server() -> impl IntoResponse {
35
35
-
let pds_hostname = pds_hostname();
36
36
-
let domains_str =
37
37
-
std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| pds_hostname.to_string());
38
38
-
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
39
39
-
let invite_code_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
40
40
-
let privacy_policy = std::env::var("PRIVACY_POLICY_URL").ok();
41
41
-
let terms_of_service = std::env::var("TERMS_OF_SERVICE_URL").ok();
42
42
-
let contact_email = std::env::var("CONTACT_EMAIL").ok();
34
34
+
let cfg = tranquil_config::get();
35
35
+
let pds_hostname = &cfg.server.hostname;
36
36
+
let domains = cfg.server.available_user_domain_list();
37
37
+
let invite_code_required = cfg.server.invite_code_required;
38
38
+
let privacy_policy = cfg.server.privacy_policy_url.clone();
39
39
+
let terms_of_service = cfg.server.terms_of_service_url.clone();
40
40
+
let contact_email = cfg.server.contact_email.clone();
43
41
let mut links = serde_json::Map::new();
44
42
if let Some(pp) = privacy_policy {
45
43
links.insert("privacyPolicy".to_string(), json!(pp));
···
2
2
use crate::api::error::DbResultExt;
3
3
use crate::auth::{Active, Auth};
4
4
use crate::state::AppState;
5
5
-
use crate::util::pds_hostname;
6
5
use axum::{
7
6
Json,
8
7
extract::State,
···
147
146
}
148
147
149
148
async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
150
150
-
let hostname = pds_hostname();
149
149
+
let hostname = &tranquil_config::get().server.hostname;
151
150
152
151
let user = match state.user_repo.get_user_for_did_doc_build(did).await {
153
152
Ok(Some(row)) => row,
···
24
24
use crate::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited};
25
25
use crate::state::AppState;
26
26
use crate::types::{Did, Handle, PlainPassword};
27
27
-
use crate::util::{pds_hostname, pds_hostname_without_port};
28
27
use crate::validation::validate_password;
29
28
30
29
fn generate_setup_token() -> String {
···
113
112
.map(|d| d.starts_with("did:web:"))
114
113
.unwrap_or(false);
115
114
116
116
-
let hostname = pds_hostname();
117
117
-
let hostname_for_handles = pds_hostname_without_port();
115
115
+
let hostname = &tranquil_config::get().server.hostname;
116
116
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
118
117
let pds_suffix = format!(".{}", hostname_for_handles);
119
118
120
119
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
···
153
152
Err(_) => return ApiError::InvalidInviteCode.into_response(),
154
153
}
155
154
} else {
156
156
-
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
155
155
+
let invite_required = tranquil_config::get().server.invite_code_required;
157
156
if invite_required {
158
157
return ApiError::InviteCodeRequired.into_response();
159
158
}
···
309
308
.into_response();
310
309
}
311
310
} else {
312
312
-
let rotation_key = std::env::var("PLC_ROTATION_KEY")
313
313
-
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&secret_key));
311
311
+
let rotation_key = tranquil_config::get()
312
312
+
.secrets
313
313
+
.plc_rotation_key
314
314
+
.clone()
315
315
+
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&secret_key));
314
316
315
317
let genesis_result = match crate::plc::create_genesis_operation(
316
318
&secret_key,
···
401
403
};
402
404
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
403
405
404
404
-
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
405
405
-
json!({
406
406
+
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
407
407
+
Some(json!({
406
408
"$type": "app.bsky.actor.defs#personalDetailsPref",
407
409
"birthDate": "1998-05-06T00:00:00.000Z"
408
408
-
})
409
409
-
});
410
410
+
}))
411
411
+
} else {
412
412
+
None
413
413
+
};
410
414
411
415
let handle_typed: Handle = match handle.parse() {
412
416
Ok(h) => h,
···
820
824
_rate_limit: RateLimited<PasswordResetLimit>,
821
825
Json(input): Json<RequestPasskeyRecoveryInput>,
822
826
) -> Response {
823
823
-
let hostname_for_handles = pds_hostname_without_port();
827
827
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
824
828
let identifier = input.email.trim().to_lowercase();
825
829
let identifier = identifier.strip_prefix('@').unwrap_or(&identifier);
826
830
let normalized_handle =
···
855
859
return ApiError::InternalError(None).into_response();
856
860
}
857
861
858
858
-
let hostname = pds_hostname();
862
862
+
let hostname = &tranquil_config::get().server.hostname;
859
863
let recovery_url = format!(
860
864
"https://{}/app/recover-passkey?did={}&token={}",
861
865
hostname,
···
7
7
use crate::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit};
8
8
use crate::state::AppState;
9
9
use crate::types::PlainPassword;
10
10
-
use crate::util::{pds_hostname, pds_hostname_without_port};
11
10
use crate::validation::validate_password;
12
11
use axum::{
13
12
Json,
···
38
37
if identifier.is_empty() {
39
38
return ApiError::InvalidRequest("email or handle is required".into()).into_response();
40
39
}
41
41
-
let hostname_for_handles = pds_hostname_without_port();
40
40
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
42
41
let normalized = identifier.to_lowercase();
43
42
let normalized = normalized.strip_prefix('@').unwrap_or(&normalized);
44
43
let is_email_lookup = normalized.contains('@');
···
78
77
error!("DB error setting reset code: {:?}", e);
79
78
return ApiError::InternalError(None).into_response();
80
79
}
81
81
-
let hostname = pds_hostname();
80
80
+
let hostname = &tranquil_config::get().server.hostname;
82
81
if let Err(e) = crate::comms::comms_repo::enqueue_password_reset(
83
82
state.user_repo.as_ref(),
84
83
state.infra_repo.as_ref(),
···
7
7
use crate::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit};
8
8
use crate::state::AppState;
9
9
use crate::types::{AccountState, Did, Handle, PlainPassword};
10
10
-
use crate::util::{pds_hostname, pds_hostname_without_port};
11
10
use axum::{
12
11
Json,
13
12
extract::State,
···
66
65
"create_session called with identifier: {}",
67
66
input.identifier
68
67
);
69
69
-
let pds_host = pds_hostname();
70
70
-
let hostname_for_handles = pds_hostname_without_port();
68
68
+
let pds_host = &tranquil_config::get().server.hostname;
69
69
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
71
70
let normalized_identifier =
72
72
-
NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles);
71
71
+
NormalizedLoginIdentifier::normalize(&input.identifier, &hostname_for_handles);
73
72
info!(
74
73
"Normalized identifier: {} -> {}",
75
74
input.identifier, normalized_identifier
···
182
181
return ApiError::LegacyLoginBlocked.into_response();
183
182
}
184
183
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
185
185
-
let hostname = pds_hostname();
184
184
+
let hostname = &tranquil_config::get().server.hostname;
186
185
if let Err(e) = crate::comms::comms_repo::enqueue_2fa_code(
187
186
state.user_repo.as_ref(),
188
187
state.infra_repo.as_ref(),
···
286
285
ip = %client_ip,
287
286
"Legacy login on TOTP-enabled account - sending notification"
288
287
);
289
289
-
let hostname = pds_hostname();
288
288
+
let hostname = &tranquil_config::get().server.hostname;
290
289
if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login(
291
290
state.user_repo.as_ref(),
292
291
state.infra_repo.as_ref(),
···
341
340
let preferred_channel_verified = row
342
341
.channel_verification
343
342
.is_verified(row.preferred_comms_channel);
344
344
-
let pds_hostname = pds_hostname();
343
343
+
let pds_hostname = &tranquil_config::get().server.hostname;
345
344
let handle = full_handle(&row.handle, pds_hostname);
346
345
let account_state = AccountState::from_db_fields(
347
346
row.deactivated_at,
···
545
544
let preferred_channel_verified = u
546
545
.channel_verification
547
546
.is_verified(u.preferred_comms_channel);
548
548
-
let pds_hostname = pds_hostname();
547
547
+
let pds_hostname = &tranquil_config::get().server.hostname;
549
548
let handle = full_handle(&u.handle, pds_hostname);
550
549
let account_state =
551
550
AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None);
···
707
706
return ApiError::InternalError(None).into_response();
708
707
}
709
708
710
710
-
let hostname = pds_hostname();
709
709
+
let hostname = &tranquil_config::get().server.hostname;
711
710
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
712
711
state.user_repo.as_ref(),
713
712
state.infra_repo.as_ref(),
···
777
776
let formatted_token =
778
777
crate::auth::verification_token::format_token_for_display(&verification_token);
779
778
780
780
-
let hostname = pds_hostname();
779
779
+
let hostname = &tranquil_config::get().server.hostname;
781
780
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
782
781
state.user_repo.as_ref(),
783
782
state.infra_repo.as_ref(),
···
9
9
use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
10
10
use crate::state::AppState;
11
11
use crate::types::PlainPassword;
12
12
-
use crate::util::pds_hostname;
13
12
use axum::{
14
13
Json,
15
14
extract::State,
···
52
51
.log_db_err("fetching handle")?
53
52
.ok_or(ApiError::AccountNotFound)?;
54
53
55
55
-
let hostname = pds_hostname();
54
54
+
let hostname = &tranquil_config::get().server.hostname;
56
55
let uri = generate_totp_uri(&secret, &handle, hostname);
57
56
58
57
let qr_code = generate_qr_png_base64(&secret, &handle, hostname).map_err(|e| {
···
5
5
use tracing::{info, warn};
6
6
7
7
use crate::state::AppState;
8
8
-
use crate::util::pds_hostname;
9
8
10
9
#[derive(Deserialize)]
11
10
#[serde(rename_all = "camelCase")]
···
71
70
return Ok(Json(ResendMigrationVerificationOutput { sent: true }));
72
71
}
73
72
74
74
-
let hostname = pds_hostname();
73
73
+
let hostname = &tranquil_config::get().server.hostname;
75
74
let token = crate::auth::verification_token::generate_migration_token(&user.did, &email);
76
75
let formatted_token = crate::auth::verification_token::format_token_for_display(&token);
77
76
···
1
1
use crate::api::error::{ApiError, DbResultExt};
2
2
use crate::comms::comms_repo;
3
3
use crate::types::Did;
4
4
-
use crate::util::pds_hostname;
5
4
use axum::{Json, extract::State};
6
5
use serde::{Deserialize, Serialize};
7
6
use tracing::{info, warn};
···
162
161
user_id,
163
162
channel,
164
163
&recipient,
165
165
-
pds_hostname(),
164
164
+
&tranquil_config::get().server.hostname,
166
165
)
167
166
.await
168
167
{
···
260
259
user.id,
261
260
channel,
262
261
&recipient,
263
263
-
pds_hostname(),
262
262
+
&tranquil_config::get().server.hostname,
264
263
)
265
264
.await
266
265
{
···
8
8
9
9
use crate::comms::comms_repo;
10
10
use crate::state::AppState;
11
11
-
use crate::util::pds_hostname;
12
11
13
12
#[derive(Deserialize)]
14
13
struct TelegramUpdate {
···
32
31
headers: HeaderMap,
33
32
body: String,
34
33
) -> impl IntoResponse {
35
35
-
let expected_secret = match std::env::var("TELEGRAM_WEBHOOK_SECRET") {
36
36
-
Ok(s) => s,
37
37
-
Err(_) => {
34
34
+
let expected_secret = match &tranquil_config::get().telegram.webhook_secret {
35
35
+
Some(s) => s.clone(),
36
36
+
None => {
38
37
warn!("Telegram webhook called but TELEGRAM_WEBHOOK_SECRET is not configured");
39
38
return StatusCode::FORBIDDEN;
40
39
}
···
88
87
user_id,
89
88
tranquil_db_traits::CommsChannel::Telegram,
90
89
&from.id.to_string(),
91
91
-
pds_hostname(),
90
90
+
&tranquil_config::get().server.hostname,
92
91
)
93
92
.await
94
93
{
···
64
64
65
65
impl DidResolver {
66
66
pub fn new() -> Self {
67
67
-
let cache_ttl_secs: u64 = std::env::var("DID_CACHE_TTL_SECS")
68
68
-
.ok()
69
69
-
.and_then(|v| v.parse().ok())
70
70
-
.unwrap_or(300);
67
67
+
let cfg = tranquil_config::get();
68
68
+
let cache_ttl_secs = cfg.plc.did_cache_ttl_secs;
71
69
72
72
-
let plc_directory_url = std::env::var("PLC_DIRECTORY_URL")
73
73
-
.unwrap_or_else(|_| "https://plc.directory".to_string());
70
70
+
let plc_directory_url = cfg.plc.directory_url.clone();
74
71
75
72
let client = Client::builder()
76
73
.timeout(Duration::from_secs(10))
···
1
1
-
use crate::util::pds_hostname;
2
1
use base64::Engine as _;
3
2
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4
3
use chrono::Utc;
···
146
145
147
146
impl ServiceTokenVerifier {
148
147
pub fn new() -> Self {
149
149
-
let plc_directory_url = std::env::var("PLC_DIRECTORY_URL")
150
150
-
.unwrap_or_else(|_| "https://plc.directory".to_string());
148
148
+
let plc_directory_url = tranquil_config::get().plc.directory_url.clone();
151
149
152
152
-
let pds_hostname = pds_hostname();
150
150
+
let pds_hostname = &tranquil_config::get().server.hostname;
153
151
let pds_did: Did = format!("did:web:{}", pds_hostname)
154
152
.parse()
155
153
.expect("PDS hostname produces a valid DID");
···
61
61
62
62
fn derive_verification_key() -> [u8; 32] {
63
63
use hkdf::Hkdf;
64
64
-
let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
65
65
-
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
66
66
-
"test-master-key-not-for-production".to_string()
67
67
-
} else {
68
68
-
panic!("MASTER_KEY must be set");
69
69
-
}
70
70
-
});
64
64
+
let master_key = tranquil_config::get().secrets.master_key_or_default();
71
65
let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
72
66
let mut key = [0u8; 32];
73
67
hk.expand(b"tranquil-pds-verification-token-v1", &mut key)
···
21
21
22
22
impl CommsService {
23
23
pub fn new(infra_repo: Arc<dyn InfraRepository>) -> Self {
24
24
-
let poll_interval_ms: u64 = std::env::var("NOTIFICATION_POLL_INTERVAL_MS")
25
25
-
.ok()
26
26
-
.and_then(|v| v.parse().ok())
27
27
-
.unwrap_or(1000);
28
28
-
let batch_size: i64 = std::env::var("NOTIFICATION_BATCH_SIZE")
29
29
-
.ok()
30
30
-
.and_then(|v| v.parse().ok())
31
31
-
.unwrap_or(100);
24
24
+
let cfg = tranquil_config::get();
25
25
+
let poll_interval_ms = cfg.notifications.poll_interval_ms;
26
26
+
let batch_size = cfg.notifications.batch_size;
32
27
Self {
33
28
infra_repo,
34
29
senders: HashMap::new(),
···
48
48
impl AuthConfig {
49
49
pub fn init() -> &'static Self {
50
50
CONFIG.get_or_init(|| {
51
51
-
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
52
52
-
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
53
53
-
"test-jwt-secret-not-for-production".to_string()
54
54
-
} else {
55
55
-
panic!(
56
56
-
"JWT_SECRET environment variable must be set in production. \
57
57
-
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
58
58
-
);
59
59
-
}
60
60
-
});
61
61
-
62
62
-
let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| {
63
63
-
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
64
64
-
"test-dpop-secret-not-for-production".to_string()
65
65
-
} else {
66
66
-
panic!(
67
67
-
"DPOP_SECRET environment variable must be set in production. \
68
68
-
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
69
69
-
);
70
70
-
}
71
71
-
});
72
72
-
73
73
-
if jwt_secret.len() < 32
74
74
-
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
75
75
-
{
76
76
-
panic!("JWT_SECRET must be at least 32 characters");
77
77
-
}
51
51
+
let secrets = &tranquil_config::get().secrets;
78
52
79
79
-
if dpop_secret.len() < 32
80
80
-
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
81
81
-
{
82
82
-
panic!("DPOP_SECRET must be at least 32 characters");
83
83
-
}
53
53
+
let jwt_secret = secrets.jwt_secret_or_default();
54
54
+
let dpop_secret = secrets.dpop_secret_or_default();
84
55
85
56
let mut hasher = Sha256::new();
86
57
hasher.update(b"oauth-signing-key-derivation:");
···
114
85
let kid_hash = kid_hasher.finalize();
115
86
let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
116
87
117
117
-
let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
118
118
-
if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
119
119
-
"test-master-key-not-for-production".to_string()
120
120
-
} else {
121
121
-
panic!(
122
122
-
"MASTER_KEY environment variable must be set in production. \
123
123
-
Set TRANQUIL_PDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
124
124
-
);
125
125
-
}
126
126
-
});
127
127
-
128
128
-
if master_key.len() < 32
129
129
-
&& std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_err()
130
130
-
{
131
131
-
panic!("MASTER_KEY must be at least 32 characters");
132
132
-
}
88
88
+
let master_key = secrets.master_key_or_default();
133
89
134
90
let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
135
91
let mut key_encryption_key = [0u8; 32];
···
1
1
use crate::circuit_breaker::CircuitBreaker;
2
2
use crate::sync::firehose::SequencedEvent;
3
3
-
use crate::util::pds_hostname;
4
3
use reqwest::Client;
5
4
use std::sync::Arc;
6
5
use std::sync::atomic::{AtomicU64, Ordering};
···
42
41
self
43
42
}
44
43
45
45
-
pub fn from_env() -> Option<Self> {
46
46
-
let hostname = pds_hostname();
44
44
+
pub fn from_config(cfg: &tranquil_config::TranquilConfig) -> Option<Self> {
45
45
+
let hostname = &cfg.server.hostname;
47
46
if hostname == "localhost" {
48
47
return None;
49
48
}
50
49
51
51
-
let crawler_urls: Vec<String> = std::env::var("CRAWLERS")
52
52
-
.unwrap_or_default()
53
53
-
.split(',')
54
54
-
.filter(|s| !s.is_empty())
55
55
-
.map(|s| s.trim().to_string())
56
56
-
.collect();
50
50
+
let crawler_urls = cfg.firehose.crawler_list();
57
51
58
52
if crawler_urls.is_empty() {
59
53
return None;
···
87
87
}
88
88
}
89
89
90
90
-
pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
90
90
+
pub fn is_service_domain_handle(handle: &str, _hostname: &str) -> bool {
91
91
if !handle.contains('.') {
92
92
return true;
93
93
}
94
94
-
let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
95
95
-
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
96
96
-
.unwrap_or_else(|_| vec![hostname.to_string()]);
94
94
+
let service_domains = tranquil_config::get().server.user_handle_domain_list();
97
95
service_domains
98
96
.iter()
99
97
.any(|domain| handle.ends_with(&format!(".{}", domain)) || handle == domain)
···
658
658
post(api::discord_webhook::handle_discord_webhook)
659
659
.layer(DefaultBodyLimit::max(64 * 1024)),
660
660
)
661
661
-
.layer(DefaultBodyLimit::max(util::get_max_blob_size()))
661
661
+
.layer(DefaultBodyLimit::max(
662
662
+
tranquil_config::get().server.max_blob_size as usize,
663
663
+
))
662
664
.layer(axum::middleware::map_response(rewrite_422_to_400))
663
665
.layer(middleware::from_fn(metrics::metrics_middleware))
664
666
.layer(
···
1
1
+
use clap::{Parser, Subcommand};
1
2
use std::net::SocketAddr;
3
3
+
use std::path::PathBuf;
2
4
use std::process::ExitCode;
3
5
use std::sync::Arc;
4
6
use tokio_util::sync::CancellationToken;
···
18
20
};
19
21
use tranquil_pds::state::AppState;
20
22
23
23
+
#[derive(Parser)]
24
24
+
#[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")]
25
25
+
struct Cli {
26
26
+
/// Path to a TOML configuration file (also settable via TRANQUIL_PDS_CONFIG env var)
27
27
+
#[arg(short, long, value_name = "FILE", env = "TRANQUIL_PDS_CONFIG")]
28
28
+
config: Option<PathBuf>,
29
29
+
30
30
+
#[command(subcommand)]
31
31
+
command: Option<Command>,
32
32
+
}
33
33
+
34
34
+
#[derive(Subcommand)]
35
35
+
enum Command {
36
36
+
/// Validate the configuration and exit
37
37
+
Validate {
38
38
+
/// Skip validation of secrets and database URL (useful when secrets
39
39
+
/// are provided at runtime via environment variables / secret files)
40
40
+
#[arg(long)]
41
41
+
ignore_secrets: bool,
42
42
+
},
43
43
+
/// Print a TOML configuration template to stdout
44
44
+
ConfigTemplate,
45
45
+
}
46
46
+
21
47
#[tokio::main]
22
48
async fn main() -> ExitCode {
23
49
dotenvy::dotenv().ok();
50
50
+
51
51
+
let cli = Cli::parse();
52
52
+
53
53
+
// Handle subcommands that don't need full startup
54
54
+
if let Some(command) = &cli.command {
55
55
+
return match command {
56
56
+
Command::ConfigTemplate => {
57
57
+
print!("{}", tranquil_config::template());
58
58
+
ExitCode::SUCCESS
59
59
+
}
60
60
+
Command::Validate { ignore_secrets } => {
61
61
+
let config = match tranquil_config::load(cli.config.as_ref()) {
62
62
+
Ok(c) => c,
63
63
+
Err(e) => {
64
64
+
eprintln!("Failed to load configuration: {e:#}");
65
65
+
return ExitCode::FAILURE;
66
66
+
}
67
67
+
};
68
68
+
match config.validate(*ignore_secrets) {
69
69
+
Ok(()) => {
70
70
+
println!("Configuration is valid.");
71
71
+
ExitCode::SUCCESS
72
72
+
}
73
73
+
Err(e) => {
74
74
+
eprint!("{e}");
75
75
+
ExitCode::FAILURE
76
76
+
}
77
77
+
}
78
78
+
}
79
79
+
};
80
80
+
}
81
81
+
24
82
tracing_subscriber::fmt::init();
83
83
+
84
84
+
let config = match tranquil_config::load(cli.config.as_ref()) {
85
85
+
Ok(c) => c,
86
86
+
Err(e) => {
87
87
+
error!("Failed to load configuration: {e:#}");
88
88
+
return ExitCode::FAILURE;
89
89
+
}
90
90
+
};
91
91
+
92
92
+
if let Err(e) = config.validate(false) {
93
93
+
error!("{e}");
94
94
+
return ExitCode::FAILURE;
95
95
+
}
96
96
+
97
97
+
tranquil_config::init(config);
98
98
+
25
99
tranquil_pds::metrics::init_metrics();
26
100
27
101
match run().await {
···
66
140
let mut comms_service = CommsService::new(state.infra_repo.clone());
67
141
let mut deferred_discord_endpoint: Option<(DiscordSender, String, String)> = None;
68
142
69
69
-
if let Some(email_sender) = EmailSender::from_env() {
143
143
+
let cfg = tranquil_config::get();
144
144
+
145
145
+
if let Some(email_sender) = EmailSender::from_config(cfg) {
70
146
info!("Email comms enabled");
71
147
comms_service = comms_service.register_sender(email_sender);
72
148
} else {
73
149
warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)");
74
150
}
75
151
76
76
-
if let Some(discord_sender) = DiscordSender::from_env() {
152
152
+
if let Some(discord_sender) = DiscordSender::from_config(cfg) {
77
153
info!("Discord comms enabled");
78
154
match discord_sender.resolve_bot_username().await {
79
155
Ok(username) => {
···
96
172
Some(public_key) => {
97
173
tranquil_pds::util::set_discord_public_key(public_key);
98
174
info!("Discord Ed25519 public key loaded");
99
99
-
let hostname = std::env::var("PDS_HOSTNAME")
100
100
-
.unwrap_or_else(|_| "localhost".to_string());
175
175
+
let hostname = &tranquil_config::get().server.hostname;
101
176
let webhook_url = format!("https://{}/webhook/discord", hostname);
102
177
match discord_sender.register_slash_command(&app_id).await {
103
178
Ok(()) => info!("Discord /start slash command registered"),
···
118
193
comms_service = comms_service.register_sender(discord_sender);
119
194
}
120
195
121
121
-
if let Some(telegram_sender) = TelegramSender::from_env() {
122
122
-
let secret_token = match std::env::var("TELEGRAM_WEBHOOK_SECRET") {
123
123
-
Ok(s) => s,
124
124
-
Err(_) => {
125
125
-
return Err(
126
126
-
"TELEGRAM_BOT_TOKEN is set but TELEGRAM_WEBHOOK_SECRET is missing. Both are required for secure Telegram integration.".into()
127
127
-
);
128
128
-
}
129
129
-
};
196
196
+
if let Some(telegram_sender) = TelegramSender::from_config(cfg) {
197
197
+
// Safe to unwrap: validated in TranquilConfig::validate()
198
198
+
let secret_token = tranquil_config::get()
199
199
+
.telegram
200
200
+
.webhook_secret
201
201
+
.clone()
202
202
+
.expect("telegram.webhook_secret checked during config validation");
130
203
info!("Telegram comms enabled");
131
204
match telegram_sender.resolve_bot_username().await {
132
205
Ok(username) => {
133
206
info!(bot_username = %username, "Resolved Telegram bot username");
134
207
tranquil_pds::util::set_telegram_bot_username(username);
135
135
-
let hostname =
136
136
-
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
208
208
+
let hostname = tranquil_config::get().server.hostname.clone();
137
209
let webhook_url = format!("https://{}/webhook/telegram", hostname);
138
210
match telegram_sender
139
211
.set_webhook(&webhook_url, Some(&secret_token))
···
150
222
comms_service = comms_service.register_sender(telegram_sender);
151
223
}
152
224
153
153
-
if let Some(signal_sender) = SignalSender::from_env() {
225
225
+
if let Some(signal_sender) = SignalSender::from_config(cfg) {
154
226
info!("Signal comms enabled");
155
227
comms_service = comms_service.register_sender(signal_sender);
156
228
}
157
229
158
230
let comms_handle = tokio::spawn(comms_service.run(shutdown.clone()));
159
231
160
160
-
let crawlers_handle = if let Some(crawlers) = Crawlers::from_env() {
232
232
+
let crawlers_handle = if let Some(crawlers) = Crawlers::from_config(cfg) {
161
233
let crawlers = Arc::new(
162
234
crawlers.with_circuit_breaker(state.circuit_breakers.relay_notification.clone()),
163
235
);
···
197
269
198
270
let app = tranquil_pds::app(state);
199
271
200
200
-
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
201
201
-
let port: u16 = std::env::var("SERVER_PORT")
202
202
-
.ok()
203
203
-
.and_then(|p| p.parse().ok())
204
204
-
.unwrap_or(3000);
272
272
+
let cfg = tranquil_config::get();
273
273
+
let host = &cfg.server.host;
274
274
+
let port = cfg.server.port;
205
275
206
276
let addr: SocketAddr = format!("{}:{}", host, port)
207
277
.parse()
···
34
34
}
35
35
36
36
fn get_extra_banned_words() -> &'static Vec<String> {
37
37
-
EXTRA_BANNED_WORDS.get_or_init(|| {
38
38
-
std::env::var("PDS_BANNED_WORDS")
39
39
-
.unwrap_or_default()
40
40
-
.split(',')
41
41
-
.map(|s| s.trim().to_lowercase())
42
42
-
.filter(|s| !s.is_empty())
43
43
-
.collect()
44
44
-
})
37
37
+
EXTRA_BANNED_WORDS.get_or_init(|| tranquil_config::get().server.banned_word_list())
45
38
}
46
39
47
40
fn strip_trailing_digits(s: &str) -> &str {
···
10
10
};
11
11
use crate::state::AppState;
12
12
use crate::types::{Did, Handle, PlainPassword};
13
13
-
use crate::util::{extract_client_ip, pds_hostname, pds_hostname_without_port};
13
13
+
use crate::util::extract_client_ip;
14
14
use axum::{
15
15
Json,
16
16
extract::{Query, State},
···
253
253
254
254
if let Some(ref login_hint) = request_data.parameters.login_hint {
255
255
tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation");
256
256
-
let hostname_for_handles = pds_hostname_without_port();
257
257
-
let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles);
256
256
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
257
257
+
let normalized = NormalizedLoginIdentifier::normalize(login_hint, &hostname_for_handles);
258
258
tracing::info!(normalized = %normalized, "Normalized login_hint");
259
259
260
260
match state
···
526
526
url_encode(error_msg)
527
527
))
528
528
};
529
529
-
let hostname_for_handles = pds_hostname_without_port();
529
529
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
530
530
let normalized_username =
531
531
-
NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles);
531
531
+
NormalizedLoginIdentifier::normalize(&form.username, &hostname_for_handles);
532
532
tracing::debug!(
533
533
original_username = %form.username,
534
534
normalized_username = %normalized_username,
535
535
-
pds_hostname = %pds_hostname(),
535
535
+
pds_hostname = %tranquil_config::get().server.hostname,
536
536
"Normalized username for lookup"
537
537
);
538
538
let user = match state
···
677
677
.await
678
678
{
679
679
Ok(challenge) => {
680
680
-
let hostname = pds_hostname();
680
680
+
let hostname = &tranquil_config::get().server.hostname;
681
681
if let Err(e) = enqueue_2fa_code(
682
682
state.user_repo.as_ref(),
683
683
state.infra_repo.as_ref(),
···
992
992
.await
993
993
{
994
994
Ok(challenge) => {
995
995
-
let hostname = pds_hostname();
995
995
+
let hostname = &tranquil_config::get().server.hostname;
996
996
if let Err(e) = enqueue_2fa_code(
997
997
state.user_repo.as_ref(),
998
998
state.infra_repo.as_ref(),
···
1116
1116
'?'
1117
1117
};
1118
1118
redirect_url.push(separator);
1119
1119
-
let pds_host = pds_hostname();
1119
1119
+
let pds_host = &tranquil_config::get().server.hostname;
1120
1120
redirect_url.push_str(&format!(
1121
1121
"iss={}",
1122
1122
url_encode(&format!("https://{}", pds_host))
···
1134
1134
state: Option<&str>,
1135
1135
response_mode: Option<&str>,
1136
1136
) -> String {
1137
1137
-
let pds_host = pds_hostname();
1137
1137
+
let pds_host = &tranquil_config::get().server.hostname;
1138
1138
let mut url = format!(
1139
1139
"https://{}/oauth/authorize/redirect?redirect_uri={}&code={}",
1140
1140
pds_host,
···
1991
1991
State(state): State<AppState>,
1992
1992
Query(query): Query<CheckPasskeysQuery>,
1993
1993
) -> Response {
1994
1994
-
let hostname_for_handles = pds_hostname_without_port();
1994
1994
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
1995
1995
let bare_identifier =
1996
1996
-
BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles);
1996
1996
+
BareLoginIdentifier::from_identifier(&query.identifier, &hostname_for_handles);
1997
1997
1998
1998
let user = state
1999
1999
.user_repo
···
2023
2023
State(state): State<AppState>,
2024
2024
Query(query): Query<CheckPasskeysQuery>,
2025
2025
) -> Response {
2026
2026
-
let hostname_for_handles = pds_hostname_without_port();
2026
2026
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
2027
2027
let normalized_identifier =
2028
2028
-
NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles);
2028
2028
+
NormalizedLoginIdentifier::normalize(&query.identifier, &hostname_for_handles);
2029
2029
2030
2030
let user = state
2031
2031
.user_repo
···
2131
2131
.into_response();
2132
2132
}
2133
2133
2134
2134
-
let hostname_for_handles = pds_hostname_without_port();
2134
2134
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
2135
2135
let normalized_username =
2136
2136
-
NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles);
2136
2136
+
NormalizedLoginIdentifier::normalize(&form.identifier, &hostname_for_handles);
2137
2137
2138
2138
let user = match state
2139
2139
.user_repo
···
2602
2602
.await
2603
2603
{
2604
2604
Ok(challenge) => {
2605
2605
-
let hostname = pds_hostname();
2605
2605
+
let hostname = &tranquil_config::get().server.hostname;
2606
2606
if let Err(e) = enqueue_2fa_code(
2607
2607
state.user_repo.as_ref(),
2608
2608
state.infra_repo.as_ref(),
···
2881
2881
headers: HeaderMap,
2882
2882
Json(form): Json<AuthorizePasskeySubmit>,
2883
2883
) -> Response {
2884
2884
-
let pds_hostname = pds_hostname();
2884
2884
+
let pds_hostname = &tranquil_config::get().server.hostname;
2885
2885
let passkey_finish_request_id = RequestId::from(form.request_uri.clone());
2886
2886
2887
2887
let request_data = match state
···
1
1
use crate::oauth::jwks::{JwkSet, create_jwk_set};
2
2
use crate::state::AppState;
3
3
-
use crate::util::pds_hostname;
4
3
use axum::{Json, extract::State};
5
4
use serde::{Deserialize, Serialize};
6
5
···
58
57
pub async fn oauth_protected_resource(
59
58
State(_state): State<AppState>,
60
59
) -> Json<ProtectedResourceMetadata> {
61
61
-
let pds_hostname = pds_hostname();
60
60
+
let pds_hostname = &tranquil_config::get().server.hostname;
62
61
let public_url = format!("https://{}", pds_hostname);
63
62
Json(ProtectedResourceMetadata {
64
63
resource: public_url.clone(),
···
72
71
pub async fn oauth_authorization_server(
73
72
State(_state): State<AppState>,
74
73
) -> Json<AuthorizationServerMetadata> {
75
75
-
let pds_hostname = pds_hostname();
74
74
+
let pds_hostname = &tranquil_config::get().server.hostname;
76
75
let issuer = format!("https://{}", pds_hostname);
77
76
Json(AuthorizationServerMetadata {
78
77
issuer: issuer.clone(),
···
12
12
verify_client_auth,
13
13
};
14
14
use crate::state::AppState;
15
15
-
use crate::util::pds_hostname;
16
15
use axum::Json;
17
16
use axum::http::{HeaderMap, Method};
18
17
use chrono::{Duration, Utc};
···
101
100
let dpop_jkt = if let Some(proof) = &dpop_proof {
102
101
let config = AuthConfig::get();
103
102
let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
104
104
-
let pds_hostname = pds_hostname();
103
103
+
let pds_hostname = &tranquil_config::get().server.hostname;
105
104
let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
106
105
let result = verifier.verify_proof(proof, Method::POST.as_str(), &token_endpoint, None)?;
107
106
if !state
···
348
347
let dpop_jkt = if let Some(proof) = &dpop_proof {
349
348
let config = AuthConfig::get();
350
349
let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
351
351
-
let pds_hostname = pds_hostname();
350
350
+
let pds_hostname = &tranquil_config::get().server.hostname;
352
351
let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
353
352
let result = verifier.verify_proof(proof, Method::POST.as_str(), &token_endpoint, None)?;
354
353
if !state
···
1
1
use crate::config::AuthConfig;
2
2
use crate::oauth::OAuthError;
3
3
-
use crate::util::pds_hostname;
4
3
use base64::Engine;
5
4
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6
5
use chrono::Utc;
···
52
51
) -> Result<String, OAuthError> {
53
52
use serde_json::json;
54
53
let jti = uuid::Uuid::new_v4().to_string();
55
55
-
let pds_hostname = pds_hostname();
54
54
+
let pds_hostname = &tranquil_config::get().server.hostname;
56
55
let issuer = format!("https://{}", pds_hostname);
57
56
let now = Utc::now().timestamp();
58
57
let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
···
2
2
use crate::oauth::OAuthError;
3
3
use crate::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited};
4
4
use crate::state::AppState;
5
5
-
use crate::util::pds_hostname;
6
5
use axum::extract::State;
7
6
use axum::http::StatusCode;
8
7
use axum::{Form, Json};
···
112
111
if token_data.expires_at < Utc::now() {
113
112
return Ok(Json(inactive_response));
114
113
}
115
115
-
let pds_hostname = pds_hostname();
114
114
+
let pds_hostname = &tranquil_config::get().server.hostname;
116
115
let issuer = format!("https://{}", pds_hostname);
117
116
Ok(Json(IntrospectResponse {
118
117
active: true,
···
124
124
}
125
125
126
126
pub fn with_cache(base_url: Option<String>, cache: Option<Arc<dyn Cache>>) -> Self {
127
127
-
let base_url = base_url.unwrap_or_else(|| {
128
128
-
std::env::var("PLC_DIRECTORY_URL")
129
129
-
.unwrap_or_else(|_| "https://plc.directory".to_string())
130
130
-
});
131
131
-
let timeout_secs: u64 = std::env::var("PLC_TIMEOUT_SECS")
132
132
-
.ok()
133
133
-
.and_then(|v| v.parse().ok())
134
134
-
.unwrap_or(10);
135
135
-
let connect_timeout_secs: u64 = std::env::var("PLC_CONNECT_TIMEOUT_SECS")
136
136
-
.ok()
137
137
-
.and_then(|v| v.parse().ok())
138
138
-
.unwrap_or(5);
127
127
+
let cfg = tranquil_config::get();
128
128
+
let base_url = base_url.unwrap_or_else(|| cfg.plc.directory_url.clone());
129
129
+
let timeout_secs = cfg.plc.timeout_secs;
130
130
+
let connect_timeout_secs = cfg.plc.connect_timeout_secs;
139
131
let client = Client::builder()
140
132
.timeout(Duration::from_secs(timeout_secs))
141
133
.connect_timeout(Duration::from_secs(connect_timeout_secs))
···
438
438
sso_repo: Arc<dyn SsoRepository>,
439
439
shutdown: CancellationToken,
440
440
) {
441
441
-
let check_interval = Duration::from_secs(
442
442
-
std::env::var("SCHEDULED_DELETE_CHECK_INTERVAL_SECS")
443
443
-
.ok()
444
444
-
.and_then(|s| s.parse().ok())
445
445
-
.unwrap_or(3600),
446
446
-
);
441
441
+
let check_interval =
442
442
+
Duration::from_secs(tranquil_config::get().scheduled.delete_check_interval_secs);
447
443
448
444
info!(
449
445
check_interval_secs = check_interval.as_secs(),
···
1
1
-
use crate::util::pds_hostname;
2
1
use std::sync::OnceLock;
3
2
use tranquil_db_traits::SsoProviderType;
4
3
···
34
33
impl SsoConfig {
35
34
pub fn init() -> &'static Self {
36
35
SSO_CONFIG.get_or_init(|| {
37
37
-
let github = Self::load_provider("GITHUB", false);
38
38
-
let discord = Self::load_provider("DISCORD", false);
39
39
-
let google = Self::load_provider("GOOGLE", false);
40
40
-
let gitlab = Self::load_provider("GITLAB", true);
41
41
-
let oidc = Self::load_provider("OIDC", true);
42
42
-
let apple = Self::load_apple_provider();
43
43
-
44
44
-
let config = SsoConfig {
45
45
-
github,
46
46
-
discord,
47
47
-
google,
48
48
-
gitlab,
49
49
-
oidc,
50
50
-
apple,
36
36
+
let sso = &tranquil_config::get().sso;
37
37
+
let config = SsoConfig {
38
38
+
github: Self::provider_from_config(
39
39
+
sso.github.enabled,
40
40
+
sso.github.client_id.as_deref(),
41
41
+
sso.github.client_secret.as_deref(),
42
42
+
None,
43
43
+
sso.github.display_name.as_deref(),
44
44
+
"GITHUB",
45
45
+
false,
46
46
+
),
47
47
+
discord: Self::provider_from_config(
48
48
+
sso.discord.enabled,
49
49
+
sso.discord.client_id.as_deref(),
50
50
+
sso.discord.client_secret.as_deref(),
51
51
+
None,
52
52
+
sso.discord.display_name.as_deref(),
53
53
+
"DISCORD",
54
54
+
false,
55
55
+
),
56
56
+
google: Self::provider_from_config(
57
57
+
sso.google.enabled,
58
58
+
sso.google.client_id.as_deref(),
59
59
+
sso.google.client_secret.as_deref(),
60
60
+
None,
61
61
+
sso.google.display_name.as_deref(),
62
62
+
"GOOGLE",
63
63
+
false,
64
64
+
),
65
65
+
gitlab: Self::provider_from_config(
66
66
+
sso.gitlab.enabled,
67
67
+
sso.gitlab.client_id.as_deref(),
68
68
+
sso.gitlab.client_secret.as_deref(),
69
69
+
sso.gitlab.issuer.as_deref(),
70
70
+
sso.gitlab.display_name.as_deref(),
71
71
+
"GITLAB",
72
72
+
true,
73
73
+
),
74
74
+
oidc: Self::provider_from_config(
75
75
+
sso.oidc.enabled,
76
76
+
sso.oidc.client_id.as_deref(),
77
77
+
sso.oidc.client_secret.as_deref(),
78
78
+
sso.oidc.issuer.as_deref(),
79
79
+
sso.oidc.display_name.as_deref(),
80
80
+
"OIDC",
81
81
+
true,
82
82
+
),
83
83
+
apple: Self::apple_from_config(&sso.apple),
51
84
};
52
85
53
86
if config.is_any_enabled() {
54
54
-
let hostname = pds_hostname();
87
87
+
let hostname = &tranquil_config::get().server.hostname;
55
88
if hostname.is_empty() || hostname == "localhost" {
56
89
panic!(
57
90
"PDS_HOSTNAME must be set to a valid hostname when SSO is enabled. \
···
72
105
})
73
106
}
74
107
75
75
-
pub fn get_redirect_uri() -> &'static str {
76
76
-
SSO_REDIRECT_URI
77
77
-
.get()
78
78
-
.map(|s| s.as_str())
79
79
-
.expect("SSO redirect URI not initialized - call SsoConfig::init() first")
80
80
-
}
81
81
-
82
82
-
fn load_provider(name: &str, needs_issuer: bool) -> Option<ProviderConfig> {
83
83
-
let enabled = crate::util::parse_env_bool(&format!("SSO_{}_ENABLED", name));
84
84
-
108
108
+
fn provider_from_config(
109
109
+
enabled: bool,
110
110
+
client_id: Option<&str>,
111
111
+
client_secret: Option<&str>,
112
112
+
issuer: Option<&str>,
113
113
+
display_name: Option<&str>,
114
114
+
name: &str,
115
115
+
needs_issuer: bool,
116
116
+
) -> Option<ProviderConfig> {
85
117
if !enabled {
86
118
return None;
87
119
}
88
88
-
89
89
-
let client_id = std::env::var(format!("SSO_{}_CLIENT_ID", name)).ok()?;
90
90
-
let client_secret = std::env::var(format!("SSO_{}_CLIENT_SECRET", name)).ok()?;
91
91
-
92
92
-
if client_id.is_empty() || client_secret.is_empty() {
93
93
-
tracing::warn!(
94
94
-
"SSO_{} enabled but missing client_id or client_secret",
95
95
-
name
96
96
-
);
97
97
-
return None;
98
98
-
}
120
120
+
let client_id = client_id.filter(|s| !s.is_empty())?;
121
121
+
let client_secret = client_secret.filter(|s| !s.is_empty())?;
99
122
100
100
-
let issuer = if needs_issuer {
101
101
-
let issuer_val = std::env::var(format!("SSO_{}_ISSUER", name)).ok();
102
102
-
if issuer_val.is_none() || issuer_val.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
123
123
+
if needs_issuer {
124
124
+
let issuer_val = issuer.filter(|s| !s.is_empty());
125
125
+
if issuer_val.is_none() {
103
126
tracing::warn!("SSO_{} requires ISSUER but none provided", name);
104
127
return None;
105
128
}
106
106
-
issuer_val
107
107
-
} else {
108
108
-
None
109
109
-
};
110
110
-
111
111
-
let display_name = std::env::var(format!("SSO_{}_NAME", name)).ok();
129
129
+
}
112
130
113
131
Some(ProviderConfig {
114
114
-
client_id,
115
115
-
client_secret,
116
116
-
issuer,
117
117
-
display_name,
132
132
+
client_id: client_id.to_string(),
133
133
+
client_secret: client_secret.to_string(),
134
134
+
issuer: issuer.map(|s| s.to_string()),
135
135
+
display_name: display_name.map(|s| s.to_string()),
118
136
})
119
137
}
120
138
121
121
-
fn load_apple_provider() -> Option<AppleProviderConfig> {
122
122
-
let enabled = crate::util::parse_env_bool("SSO_APPLE_ENABLED");
123
123
-
124
124
-
if !enabled {
139
139
+
fn apple_from_config(cfg: &tranquil_config::SsoAppleConfig) -> Option<AppleProviderConfig> {
140
140
+
if !cfg.enabled {
125
141
return None;
126
142
}
127
127
-
128
128
-
let client_id = std::env::var("SSO_APPLE_CLIENT_ID").ok()?;
129
129
-
let team_id = std::env::var("SSO_APPLE_TEAM_ID").ok()?;
130
130
-
let key_id = std::env::var("SSO_APPLE_KEY_ID").ok()?;
131
131
-
let private_key_pem = std::env::var("SSO_APPLE_PRIVATE_KEY").ok()?;
143
143
+
let client_id = cfg.client_id.as_deref().filter(|s| !s.is_empty())?;
144
144
+
let team_id = cfg.team_id.as_deref().filter(|s| !s.is_empty())?;
145
145
+
let key_id = cfg.key_id.as_deref().filter(|s| !s.is_empty())?;
146
146
+
let private_key_pem = cfg.private_key.as_deref().filter(|s| !s.is_empty())?;
132
147
133
133
-
if client_id.is_empty() {
134
134
-
tracing::warn!("SSO_APPLE enabled but missing CLIENT_ID");
135
135
-
return None;
136
136
-
}
137
137
-
if team_id.is_empty() || team_id.len() != 10 {
148
148
+
if team_id.len() != 10 {
138
149
tracing::warn!("SSO_APPLE enabled but TEAM_ID is invalid (must be 10 characters)");
139
150
return None;
140
151
}
141
141
-
if key_id.is_empty() {
142
142
-
tracing::warn!("SSO_APPLE enabled but missing KEY_ID");
143
143
-
return None;
144
144
-
}
145
145
-
if private_key_pem.is_empty() || !private_key_pem.contains("PRIVATE KEY") {
152
152
+
if !private_key_pem.contains("PRIVATE KEY") {
146
153
tracing::warn!("SSO_APPLE enabled but PRIVATE_KEY is invalid");
147
154
return None;
148
155
}
149
156
150
157
Some(AppleProviderConfig {
151
151
-
client_id,
152
152
-
team_id,
153
153
-
key_id,
154
154
-
private_key_pem,
158
158
+
client_id: client_id.to_string(),
159
159
+
team_id: team_id.to_string(),
160
160
+
key_id: key_id.to_string(),
161
161
+
private_key_pem: private_key_pem.to_string(),
155
162
})
163
163
+
}
164
164
+
165
165
+
pub fn get_redirect_uri() -> &'static str {
166
166
+
SSO_REDIRECT_URI
167
167
+
.get()
168
168
+
.map(|s| s.as_str())
169
169
+
.expect("SSO redirect URI not initialized - call SsoConfig::init() first")
156
170
}
157
171
158
172
pub fn get() -> &'static Self {
···
18
18
check_user_rate_limit_with_message,
19
19
};
20
20
use crate::state::AppState;
21
21
-
use crate::util::{pds_hostname, pds_hostname_without_port};
22
21
23
22
fn generate_state() -> String {
24
23
use rand::RngCore;
···
773
772
}
774
773
};
775
774
776
776
-
let hostname_for_handles = pds_hostname_without_port();
775
775
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
777
776
let full_handle = format!("{}.{}", validated, hostname_for_handles);
778
777
let handle_typed: crate::types::Handle = match full_handle.parse() {
779
778
Ok(h) => h,
···
856
855
.await?
857
856
.ok_or(ApiError::SsoSessionExpired)?;
858
857
859
859
-
let hostname = pds_hostname();
860
860
-
let hostname_for_handles = pds_hostname_without_port();
858
858
+
let hostname = &tranquil_config::get().server.hostname;
859
859
+
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
861
860
862
861
let handle = match crate::api::validation::validate_short_handle(&input.handle) {
863
862
Ok(h) => format!("{}.{}", h, hostname_for_handles),
···
948
947
Err(_) => return Err(ApiError::InvalidInviteCode),
949
948
}
950
949
} else {
951
951
-
let invite_required = crate::util::parse_env_bool("INVITE_CODE_REQUIRED");
950
950
+
let invite_required = tranquil_config::get().server.invite_code_required;
952
951
if invite_required {
953
952
return Err(ApiError::InviteCodeRequired);
954
953
}
···
1006
1005
d.to_string()
1007
1006
}
1008
1007
_ => {
1009
1009
-
let rotation_key = std::env::var("PLC_ROTATION_KEY")
1010
1010
-
.unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
1008
1008
+
let rotation_key = tranquil_config::get()
1009
1009
+
.secrets
1010
1010
+
.plc_rotation_key
1011
1011
+
.clone()
1012
1012
+
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
1011
1013
1012
1014
let genesis_result = match crate::plc::create_genesis_operation(
1013
1015
&signing_key,
···
1085
1087
1086
1088
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
1087
1089
1088
1088
-
let birthdate_pref = std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").ok().map(|_| {
1089
1089
-
json!({
1090
1090
+
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
1091
1091
+
Some(json!({
1090
1092
"$type": "app.bsky.actor.defs#personalDetailsPref",
1091
1093
"birthDate": "1998-05-06T00:00:00.000Z"
1092
1092
-
})
1093
1093
-
});
1094
1094
+
}))
1095
1095
+
} else {
1096
1096
+
None
1097
1097
+
};
1094
1098
1095
1099
let create_input = tranquil_db_traits::CreateSsoAccountInput {
1096
1100
handle: handle_typed.clone(),
···
1299
1303
return Err(ApiError::InternalError(None));
1300
1304
}
1301
1305
1302
1302
-
let hostname = pds_hostname();
1306
1306
+
let hostname = &tranquil_config::get().server.hostname;
1303
1307
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
1304
1308
state.user_repo.as_ref(),
1305
1309
state.infra_repo.as_ref(),
···
1
1
use crate::appview::DidResolver;
2
2
use crate::auth::webauthn::WebAuthnConfig;
3
3
-
use crate::cache::{Cache, DistributedRateLimiter, create_cache};
3
3
+
use crate::cache::{create_cache, Cache, DistributedRateLimiter};
4
4
use crate::circuit_breaker::CircuitBreakers;
5
5
use crate::config::AuthConfig;
6
6
use crate::rate_limit::RateLimiters;
7
7
use crate::repo::PostgresBlockStore;
8
8
use crate::repo_write_lock::RepoWriteLocks;
9
9
use crate::sso::{SsoConfig, SsoManager};
10
10
-
use crate::storage::{BackupStorage, BlobStorage, create_backup_storage, create_blob_storage};
10
10
+
use crate::storage::{create_backup_storage, create_blob_storage, BackupStorage, BlobStorage};
11
11
use crate::sync::firehose::SequencedEvent;
12
12
-
use crate::util::pds_hostname;
13
12
use sqlx::PgPool;
14
13
use std::error::Error;
14
14
+
use std::sync::atomic::{AtomicBool, Ordering};
15
15
use std::sync::Arc;
16
16
-
use std::sync::atomic::{AtomicBool, Ordering};
17
16
use tokio::sync::broadcast;
18
17
use tokio_util::sync::CancellationToken;
19
18
use tranquil_db::{
···
25
24
static RATE_LIMITING_DISABLED: AtomicBool = AtomicBool::new(false);
26
25
27
26
pub fn init_rate_limit_override() {
28
28
-
let disabled = std::env::var("DISABLE_RATE_LIMITING").is_ok();
27
27
+
let disabled = tranquil_config::get().server.disable_rate_limiting;
29
28
RATE_LIMITING_DISABLED.store(disabled, Ordering::Relaxed);
30
29
if disabled {
31
31
-
tracing::warn!("rate limiting is DISABLED via DISABLE_RATE_LIMITING env var");
30
30
+
tracing::warn!("rate limiting is DISABLED via configuration");
32
31
}
33
32
}
34
33
···
205
204
206
205
impl AppState {
207
206
pub async fn new(shutdown: CancellationToken) -> Result<Self, Box<dyn Error>> {
208
208
-
let database_url = std::env::var("DATABASE_URL")
209
209
-
.map_err(|_| "DATABASE_URL environment variable must be set")?;
210
210
-
211
211
-
let max_connections: u32 = std::env::var("DATABASE_MAX_CONNECTIONS")
212
212
-
.ok()
213
213
-
.and_then(|v| v.parse().ok())
214
214
-
.unwrap_or(100);
215
215
-
216
216
-
let min_connections: u32 = std::env::var("DATABASE_MIN_CONNECTIONS")
217
217
-
.ok()
218
218
-
.and_then(|v| v.parse().ok())
219
219
-
.unwrap_or(10);
220
220
-
221
221
-
let acquire_timeout_secs: u64 = std::env::var("DATABASE_ACQUIRE_TIMEOUT_SECS")
222
222
-
.ok()
223
223
-
.and_then(|v| v.parse().ok())
224
224
-
.unwrap_or(10);
207
207
+
let cfg = tranquil_config::get();
208
208
+
let database_url = &cfg.database.url;
209
209
+
let max_connections = cfg.database.max_connections;
210
210
+
let min_connections = cfg.database.min_connections;
211
211
+
let acquire_timeout_secs = cfg.database.acquire_timeout_secs;
225
212
226
213
tracing::info!(
227
214
"Configuring database pool: max={}, min={}, acquire_timeout={}s",
···
257
244
let blob_store = create_blob_storage().await;
258
245
let backup_storage = create_backup_storage().await;
259
246
260
260
-
let firehose_buffer_size: usize = std::env::var("FIREHOSE_BUFFER_SIZE")
261
261
-
.ok()
262
262
-
.and_then(|v| v.parse().ok())
263
263
-
.unwrap_or(10000);
247
247
+
let firehose_buffer_size = tranquil_config::get().firehose.buffer_size;
264
248
265
249
let (firehose_tx, _) = broadcast::channel(firehose_buffer_size);
266
250
let rate_limiters = Arc::new(RateLimiters::new());
···
271
255
let sso_config = SsoConfig::init();
272
256
let sso_manager = SsoManager::from_config(sso_config);
273
257
let webauthn_config = Arc::new(
274
274
-
WebAuthnConfig::new(pds_hostname())
258
258
+
WebAuthnConfig::new(&tranquil_config::get().server.hostname)
275
259
.expect("Failed to create WebAuthn config at startup"),
276
260
);
277
261
···
59
59
}
60
60
61
61
fn get_backfill_hours() -> i64 {
62
62
-
std::env::var("FIREHOSE_BACKFILL_HOURS")
63
63
-
.ok()
64
64
-
.and_then(|v| v.parse().ok())
65
65
-
.unwrap_or(72)
62
62
+
tranquil_config::get().firehose.backfill_hours
66
63
}
67
64
68
65
async fn handle_socket_inner(
···
204
201
}
205
202
}
206
203
}
207
207
-
let max_lag_before_disconnect: u64 = std::env::var("FIREHOSE_MAX_LAG")
208
208
-
.ok()
209
209
-
.and_then(|v| v.parse().ok())
210
210
-
.unwrap_or(5000);
204
204
+
let max_lag_before_disconnect: u64 = tranquil_config::get().firehose.max_lag;
211
205
loop {
212
206
tokio::select! {
213
207
result = rx.recv() => {
···
145
145
}
146
146
147
147
async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> {
148
148
-
let plc_url = std::env::var("PLC_DIRECTORY_URL")
149
149
-
.unwrap_or_else(|_| "https://plc.directory".to_string());
148
148
+
let plc_url = tranquil_config::get().plc.directory_url.clone();
150
149
let url = format!("{}/{}", plc_url, urlencoding::encode(did));
151
150
let response = self
152
151
.http_client
···
9
9
use std::sync::OnceLock;
10
10
11
11
const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567";
12
12
-
const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024;
13
12
14
14
-
static MAX_BLOB_SIZE: OnceLock<usize> = OnceLock::new();
15
15
-
static PDS_HOSTNAME: OnceLock<String> = OnceLock::new();
16
16
-
static PDS_HOSTNAME_WITHOUT_PORT: OnceLock<String> = OnceLock::new();
17
13
static DISCORD_BOT_USERNAME: OnceLock<String> = OnceLock::new();
18
14
static DISCORD_PUBLIC_KEY: OnceLock<ed25519_dalek::VerifyingKey> = OnceLock::new();
19
15
static DISCORD_APP_ID: OnceLock<String> = OnceLock::new();
20
16
static TELEGRAM_BOT_USERNAME: OnceLock<String> = OnceLock::new();
21
21
-
22
22
-
pub fn get_max_blob_size() -> usize {
23
23
-
*MAX_BLOB_SIZE.get_or_init(|| {
24
24
-
std::env::var("MAX_BLOB_SIZE")
25
25
-
.ok()
26
26
-
.and_then(|s| s.parse().ok())
27
27
-
.unwrap_or(DEFAULT_MAX_BLOB_SIZE)
28
28
-
})
29
29
-
}
30
17
31
18
pub fn generate_token_code() -> String {
32
19
generate_token_code_parts(2, 5)
···
109
96
.unwrap_or_else(|| "unknown".to_string())
110
97
}
111
98
112
112
-
pub fn pds_hostname() -> &'static str {
113
113
-
PDS_HOSTNAME
114
114
-
.get_or_init(|| std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()))
115
115
-
}
116
116
-
117
117
-
pub fn pds_hostname_without_port() -> &'static str {
118
118
-
PDS_HOSTNAME_WITHOUT_PORT.get_or_init(|| {
119
119
-
let hostname = pds_hostname();
120
120
-
hostname.split(':').next().unwrap_or(hostname).to_string()
121
121
-
})
122
122
-
}
123
123
-
124
99
pub fn set_discord_bot_username(username: String) {
125
100
DISCORD_BOT_USERNAME.set(username).ok();
126
101
}
···
154
129
}
155
130
156
131
pub fn parse_env_bool(key: &str) -> bool {
132
132
+
// Check the config system first, then fall back to env var for dynamic
133
133
+
// SSO keys that are not in the static config struct.
157
134
std::env::var(key)
158
135
.map(|v| v == "true" || v == "1")
159
136
.unwrap_or(false)
160
160
-
}
161
161
-
162
162
-
pub fn pds_public_url() -> String {
163
163
-
format!("https://{}", pds_hostname())
164
137
}
165
138
166
139
pub fn build_full_url(path: &str) -> String {
140
140
+
let cfg = tranquil_config::get();
167
141
let normalized_path = if !path.starts_with("/xrpc/")
168
142
&& (path.starts_with("/com.atproto.")
169
143
|| path.starts_with("/app.bsky.")
170
144
|| path.starts_with("/_"))
171
145
{
172
172
-
format!("/xrpc{}", path)
146
146
+
format!("/xrpc{path}")
173
147
} else {
174
148
path.to_string()
175
149
};
176
176
-
format!("{}{}", pds_public_url(), normalized_path)
150
150
+
format!("{}{normalized_path}", cfg.server.public_url())
177
151
}
178
152
179
153
pub fn json_to_ipld(value: &JsonValue) -> Ipld {
···
5
5
license.workspace = true
6
6
7
7
[dependencies]
8
8
+
tranquil-config = { workspace = true }
8
9
tranquil-infra = { workspace = true }
9
10
10
11
async-trait = { workspace = true }
···
15
15
pub cache_max_bytes: usize,
16
16
}
17
17
18
18
-
fn parse_env_with_warning<T: std::str::FromStr>(var_name: &str, raw: &str) -> Option<T> {
19
19
-
match raw.parse::<T>() {
20
20
-
Ok(v) => Some(v),
21
21
-
Err(_) => {
22
22
-
tracing::warn!(
23
23
-
var = var_name,
24
24
-
value = raw,
25
25
-
"invalid env var value, using default"
26
26
-
);
27
27
-
None
28
28
-
}
29
29
-
}
30
30
-
}
18
18
+
impl RippleConfig {
19
19
+
pub fn from_config() -> Result<Self, RippleConfigError> {
20
20
+
let ripple = &tranquil_config::get().cache.ripple;
31
21
32
32
-
impl RippleConfig {
33
33
-
pub fn from_env() -> Result<Self, RippleConfigError> {
34
34
-
let bind_addr: SocketAddr = std::env::var("RIPPLE_BIND")
35
35
-
.unwrap_or_else(|_| "0.0.0.0:0".into())
22
22
+
let bind_addr: SocketAddr = ripple
23
23
+
.bind_addr
36
24
.parse()
37
25
.map_err(|e| RippleConfigError::InvalidAddr(format!("{e}")))?;
38
26
39
39
-
let seed_peers: Vec<SocketAddr> = std::env::var("RIPPLE_PEERS")
40
40
-
.unwrap_or_default()
41
41
-
.split(',')
27
27
+
let seed_peers: Vec<SocketAddr> = ripple
28
28
+
.peers
29
29
+
.as_deref()
30
30
+
.unwrap_or(&[])
31
31
+
.iter()
42
32
.filter(|s| !s.trim().is_empty())
43
33
.map(|s| {
44
34
s.trim()
···
47
37
})
48
38
.collect::<Result<Vec<_>, _>>()?;
49
39
50
50
-
let machine_id: u64 = std::env::var("RIPPLE_MACHINE_ID")
51
51
-
.ok()
52
52
-
.and_then(|v| parse_env_with_warning::<u64>("RIPPLE_MACHINE_ID", &v))
53
53
-
.unwrap_or_else(|| {
54
54
-
let host_str = std::fs::read_to_string("/etc/hostname")
55
55
-
.map(|s| s.trim().to_string())
56
56
-
.unwrap_or_else(|_| format!("pid-{}", std::process::id()));
57
57
-
let input = format!("{host_str}:{bind_addr}:{}", std::process::id());
58
58
-
fnv1a(input.as_bytes())
59
59
-
});
60
60
-
61
61
-
let gossip_interval_ms: u64 = std::env::var("RIPPLE_GOSSIP_INTERVAL_MS")
62
62
-
.ok()
63
63
-
.and_then(|v| parse_env_with_warning::<u64>("RIPPLE_GOSSIP_INTERVAL_MS", &v))
64
64
-
.unwrap_or(200)
65
65
-
.max(50);
40
40
+
let machine_id = ripple.machine_id.unwrap_or_else(|| {
41
41
+
let host_str = std::fs::read_to_string("/etc/hostname")
42
42
+
.map(|s| s.trim().to_string())
43
43
+
.unwrap_or_else(|_| format!("pid-{}", std::process::id()));
44
44
+
let input = format!("{host_str}:{bind_addr}:{}", std::process::id());
45
45
+
fnv1a(input.as_bytes())
46
46
+
});
66
47
67
67
-
let cache_max_mb: usize = std::env::var("RIPPLE_CACHE_MAX_MB")
68
68
-
.ok()
69
69
-
.and_then(|v| parse_env_with_warning::<usize>("RIPPLE_CACHE_MAX_MB", &v))
70
70
-
.unwrap_or(256)
71
71
-
.clamp(1, 16_384);
48
48
+
let gossip_interval_ms = ripple.gossip_interval_ms.max(50);
72
49
73
73
-
let cache_max_bytes = cache_max_mb.saturating_mul(1024).saturating_mul(1024);
50
50
+
let cache_max_bytes = ripple
51
51
+
.cache_max_mb
52
52
+
.clamp(1, 16_384)
53
53
+
.saturating_mul(1024)
54
54
+
.saturating_mul(1024);
74
55
75
56
Ok(Self {
76
57
bind_addr,
···
9
9
s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
10
10
11
11
[dependencies]
12
12
+
tranquil-config = { workspace = true }
12
13
tranquil-infra = { workspace = true }
13
14
14
15
async-trait = { workspace = true }
···
121
121
122
122
impl S3BlobStorage {
123
123
pub async fn new() -> Self {
124
124
-
let bucket = std::env::var("S3_BUCKET").expect("S3_BUCKET must be set");
124
124
+
let cfg = tranquil_config::get();
125
125
+
let bucket = cfg
126
126
+
.storage
127
127
+
.s3_bucket
128
128
+
.clone()
129
129
+
.expect("storage.s3_bucket (S3_BUCKET) must be set");
125
130
let client = create_s3_client().await;
126
131
Self { client, bucket }
127
132
}
···
140
145
.load()
141
146
.await;
142
147
143
143
-
std::env::var("S3_ENDPOINT").ok().map_or_else(
144
144
-
|| Client::new(&config),
145
145
-
|endpoint| {
146
146
-
let s3_config = aws_sdk_s3::config::Builder::from(&config)
147
147
-
.endpoint_url(endpoint)
148
148
-
.force_path_style(true)
149
149
-
.build();
150
150
-
Client::from_conf(s3_config)
151
151
-
},
152
152
-
)
148
148
+
tranquil_config::get()
149
149
+
.storage
150
150
+
.s3_endpoint
151
151
+
.as_deref()
152
152
+
.map_or_else(
153
153
+
|| Client::new(&config),
154
154
+
|endpoint| {
155
155
+
let s3_config = aws_sdk_s3::config::Builder::from(&config)
156
156
+
.endpoint_url(endpoint)
157
157
+
.force_path_style(true)
158
158
+
.build();
159
159
+
Client::from_conf(s3_config)
160
160
+
},
161
161
+
)
153
162
}
154
163
155
164
pub struct S3BackupStorage {
···
159
168
160
169
impl S3BackupStorage {
161
170
pub async fn new() -> Option<Self> {
162
162
-
let bucket = std::env::var("BACKUP_S3_BUCKET").ok()?;
171
171
+
let bucket = tranquil_config::get().backup.s3_bucket.clone()?;
163
172
let client = create_s3_client().await;
164
173
Some(Self { client, bucket })
165
174
}
···
499
508
})
500
509
}
501
510
502
502
-
pub async fn from_env() -> Result<Self, StorageError> {
503
503
-
let path = std::env::var("BLOB_STORAGE_PATH")
504
504
-
.map_err(|_| StorageError::Other("BLOB_STORAGE_PATH not set".into()))?;
505
505
-
Self::new(path).await
506
506
-
}
507
507
-
508
511
fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> {
509
512
validate_key(key)?;
510
513
Ok(split_cid_path(key).map_or_else(
···
649
652
})
650
653
}
651
654
652
652
-
pub async fn from_env() -> Result<Self, StorageError> {
653
653
-
let path = std::env::var("BACKUP_STORAGE_PATH")
654
654
-
.map_err(|_| StorageError::Other("BACKUP_STORAGE_PATH not set".into()))?;
655
655
-
Self::new(path).await
656
656
-
}
657
657
-
658
655
fn resolve_path(&self, key: &str) -> Result<PathBuf, StorageError> {
659
656
validate_key(key)?;
660
657
Ok(self.base_path.join(key))
···
701
698
}
702
699
703
700
pub async fn create_blob_storage() -> Arc<dyn BlobStorage> {
704
704
-
let backend = std::env::var("BLOB_STORAGE_BACKEND").unwrap_or_else(|_| "filesystem".into());
701
701
+
let cfg = tranquil_config::get();
702
702
+
let backend = &cfg.storage.backend;
705
703
706
704
match backend.as_str() {
707
705
#[cfg(feature = "s3")]
···
718
716
}
719
717
_ => {
720
718
tracing::info!("Initializing filesystem blob storage");
721
721
-
FilesystemBlobStorage::from_env()
719
719
+
let path = cfg.storage.path.clone();
720
720
+
FilesystemBlobStorage::new(path)
722
721
.await
723
722
.unwrap_or_else(|e| {
724
723
panic!(
···
733
732
}
734
733
735
734
pub async fn create_backup_storage() -> Option<Arc<dyn BackupStorage>> {
736
736
-
let enabled = std::env::var("BACKUP_ENABLED")
737
737
-
.map(|v| v != "false" && v != "0")
738
738
-
.unwrap_or(true);
735
735
+
let cfg = tranquil_config::get();
739
736
740
740
-
if !enabled {
741
741
-
tracing::info!("Backup storage disabled via BACKUP_ENABLED=false");
737
737
+
if !cfg.backup.enabled {
738
738
+
tracing::info!("Backup storage disabled via config");
742
739
return None;
743
740
}
744
741
745
745
-
let backend = std::env::var("BACKUP_STORAGE_BACKEND").unwrap_or_else(|_| "filesystem".into());
742
742
+
let backend = &cfg.backup.backend;
746
743
747
744
match backend.as_str() {
748
745
#[cfg(feature = "s3")]
···
767
764
);
768
765
None
769
766
}
770
770
-
_ => FilesystemBackupStorage::from_env().await.map_or_else(
767
767
+
_ => {
768
768
+
let path = cfg.backup.path.clone();
769
769
+
FilesystemBackupStorage::new(path).await.map_or_else(
771
770
|e| {
772
771
tracing::error!(
773
772
"Failed to initialize filesystem backup storage: {}. \
···
781
780
tracing::info!("Initialized filesystem backup storage");
782
781
Some(Arc::new(storage) as Arc<dyn BackupStorage>)
783
782
},
784
784
-
),
783
783
+
)
784
784
+
}
785
785
}
786
786
}
787
787
···
17
17
MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}"
18
18
CRAWLERS: "${CRAWLERS:-https://bsky.network}"
19
19
volumes:
20
20
+
- ./config.toml:/etc/tranquil-pds/config.toml:ro
20
21
- blob_data:/var/lib/tranquil/blobs
21
22
- backup_data:/var/lib/tranquil/backups
22
23
depends_on:
···
11
11
environment:
12
12
DATABASE_URL: postgres://postgres:postgres@db:5432/pds
13
13
volumes:
14
14
+
- ./config.toml:/etc/tranquil-pds/config.toml:ro
14
15
- blob_data:/var/lib/tranquil/blobs
15
16
- backup_data:/var/lib/tranquil/backups
16
17
depends_on:
···
18
18
If you just want to get running quickly:
19
19
20
20
```sh
21
21
-
cp .env.example .env
21
21
+
cp example.toml config.toml
22
22
```
23
23
24
24
-
Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
24
24
+
Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
25
25
26
26
Build and start:
27
27
```sh
···
59
59
```sh
60
60
podman run -d --name tranquil-pds \
61
61
--network=host \
62
62
-
--env-file /etc/tranquil-pds/tranquil-pds.env \
62
62
+
-v /etc/tranquil-pds/config.toml:/etc/tranquil-pds/config.toml:ro,Z \
63
63
-v /var/lib/tranquil:/var/lib/tranquil:Z \
64
64
tranquil-pds:latest
65
65
```
···
113
113
mkdir -p /srv/tranquil-pds/{postgres,blobs,backups,certs,acme,config}
114
114
```
115
115
116
116
-
## Create an environment file
116
116
+
## Create a configuration file
117
117
118
118
```bash
119
119
-
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
120
120
-
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
119
119
+
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
120
120
+
chmod 600 /srv/tranquil-pds/config/config.toml
121
121
```
122
122
123
123
-
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
123
123
+
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
124
124
```bash
125
125
openssl rand -base64 48
126
126
```
127
127
+
128
128
+
> **Note:** Every config option can also be set via environment variables
129
129
+
> (see comments in `example.toml`). Environment variables always take
130
130
+
> precedence over the config file.
127
131
128
132
## Install quadlet definitions
129
133
···
157
161
## Create podman secrets
158
162
159
163
```bash
160
160
-
source /srv/tranquil-pds/config/tranquil-pds.env
161
164
echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
162
165
```
163
166
···
264
267
podman build -t tranquil-pds-frontend:latest ./frontend
265
268
```
266
269
267
267
-
## Create an environment file
270
270
+
## Create a configuration file
268
271
269
272
```sh
270
270
-
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
271
271
-
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
273
273
+
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
274
274
+
chmod 600 /srv/tranquil-pds/config/config.toml
272
275
```
273
276
274
274
-
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
277
277
+
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
275
278
```sh
276
279
openssl rand -base64 48
277
280
```
281
281
+
282
282
+
> **Note:** Every config option can also be set via environment variables
283
283
+
> (see comments in `example.toml`). Environment variables always take
284
284
+
> precedence over the config file.
278
285
279
286
## Set up compose and nginx
280
287
···
308
315
after firewall
309
316
}
310
317
start_pre() {
311
311
-
set -a
312
312
-
. /srv/tranquil-pds/config/tranquil-pds.env
313
313
-
set +a
318
318
+
checkpath -d /srv/tranquil-pds
314
319
}
315
320
stop() {
316
321
ebegin "Stopping ${name}"
317
322
cd /srv/tranquil-pds
318
318
-
set -a
319
319
-
. /srv/tranquil-pds/config/tranquil-pds.env
320
320
-
set +a
321
323
podman-compose -f /srv/tranquil-pds/docker-compose.yml down
322
324
eend $?
323
325
}
···
73
73
74
74
```bash
75
75
mkdir -p /etc/tranquil-pds
76
76
-
cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env
77
77
-
chmod 600 /etc/tranquil-pds/tranquil-pds.env
76
76
+
cp /opt/tranquil-pds/example.toml /etc/tranquil-pds/config.toml
77
77
+
chmod 600 /etc/tranquil-pds/config.toml
78
78
```
79
79
80
80
-
Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with:
80
80
+
Edit `/etc/tranquil-pds/config.toml` and fill in your values. Generate secrets with:
81
81
```bash
82
82
openssl rand -base64 48
83
83
+
```
84
84
+
85
85
+
> **Note:** Every config option can also be set via environment variables
86
86
+
> (see comments in `example.toml`). Environment variables always take
87
87
+
> precedence over the config file. You can also pass the config file path
88
88
+
> via the `TRANQUIL_PDS_CONFIG` env var instead of `--config`.
89
89
+
90
90
+
You can validate your configuration before starting the service:
91
91
+
```bash
92
92
+
/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate
83
93
```
84
94
85
95
## Install frontend files
···
105
115
Type=simple
106
116
User=tranquil-pds
107
117
Group=tranquil-pds
108
108
-
EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env
109
109
-
ExecStart=/usr/local/bin/tranquil-pds
118
118
+
ExecStart=/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml
110
119
Restart=always
111
120
RestartSec=5
112
121
ProtectSystem=strict
···
9
9
You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com`. User handles are served as subdomains.
10
10
11
11
The container image expects:
12
12
+
- A TOML config file mounted at `/etc/tranquil-pds/config.toml` (or passed via `--config`)
12
13
- `DATABASE_URL` - postgres connection string
13
14
- `BLOB_STORAGE_PATH` - path to blob storage (mount a PV here)
14
15
- `BACKUP_STORAGE_PATH` - path for repo backups (optional but recommended)
15
16
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
16
17
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
17
18
- `CRAWLERS` - typically `https://bsky.network`
18
18
-
and more, check the .env.example.
19
19
+
20
20
+
and more, check the example.toml for all options. Environment variables can override any TOML value.
21
21
+
You can also point to a config file via the `TRANQUIL_PDS_CONFIG` env var.
19
22
20
23
Health check: `GET /xrpc/_health`
21
24
···
1
1
+
[server]
2
2
+
# Public hostname of the PDS (e.g. `pds.example.com`).
3
3
+
#
4
4
+
# Can also be specified via environment variable `PDS_HOSTNAME`.
5
5
+
#
6
6
+
# Required! This value must be specified.
7
7
+
#hostname =
8
8
+
9
9
+
# Address to bind the HTTP server to.
10
10
+
#
11
11
+
# Can also be specified via environment variable `SERVER_HOST`.
12
12
+
#
13
13
+
# Default value: "127.0.0.1"
14
14
+
#host = "127.0.0.1"
15
15
+
16
16
+
# Port to bind the HTTP server to.
17
17
+
#
18
18
+
# Can also be specified via environment variable `SERVER_PORT`.
19
19
+
#
20
20
+
# Default value: 3000
21
21
+
#port = 3000
22
22
+
23
23
+
# List of domains for user handles.
24
24
+
# Defaults to the PDS hostname when not set.
25
25
+
#
26
26
+
# Can also be specified via environment variable `PDS_USER_HANDLE_DOMAINS`.
27
27
+
#user_handle_domains =
28
28
+
29
29
+
# List of domains available for user registration.
30
30
+
# Defaults to the PDS hostname when not set.
31
31
+
#
32
32
+
# Can also be specified via environment variable `AVAILABLE_USER_DOMAINS`.
33
33
+
#available_user_domains =
34
34
+
35
35
+
# Enable PDS-hosted did:web identities. Hosting did:web requires a
36
36
+
# long-term commitment to serve DID documents; opt-in only.
37
37
+
#
38
38
+
# Can also be specified via environment variable `ENABLE_PDS_HOSTED_DID_WEB`.
39
39
+
#
40
40
+
# Default value: false
41
41
+
#enable_pds_hosted_did_web = false
42
42
+
43
43
+
# When set to true, skip age-assurance birthday prompt for all accounts.
44
44
+
#
45
45
+
# Can also be specified via environment variable `PDS_AGE_ASSURANCE_OVERRIDE`.
46
46
+
#
47
47
+
# Default value: false
48
48
+
#age_assurance_override = false
49
49
+
50
50
+
# Require an invite code for new account registration.
51
51
+
#
52
52
+
# Can also be specified via environment variable `INVITE_CODE_REQUIRED`.
53
53
+
#
54
54
+
# Default value: true
55
55
+
#invite_code_required = true
56
56
+
57
57
+
# Allow HTTP (non-TLS) proxy requests. Only useful during development.
58
58
+
#
59
59
+
# Can also be specified via environment variable `ALLOW_HTTP_PROXY`.
60
60
+
#
61
61
+
# Default value: false
62
62
+
#allow_http_proxy = false
63
63
+
64
64
+
# Disable all rate limiting. Should only be used in testing.
65
65
+
#
66
66
+
# Can also be specified via environment variable `DISABLE_RATE_LIMITING`.
67
67
+
#
68
68
+
# Default value: false
69
69
+
#disable_rate_limiting = false
70
70
+
71
71
+
# List of additional banned words for handle validation.
72
72
+
#
73
73
+
# Can also be specified via environment variable `PDS_BANNED_WORDS`.
74
74
+
#banned_words =
75
75
+
76
76
+
# URL to a privacy policy page.
77
77
+
#
78
78
+
# Can also be specified via environment variable `PRIVACY_POLICY_URL`.
79
79
+
#privacy_policy_url =
80
80
+
81
81
+
# URL to terms of service page.
82
82
+
#
83
83
+
# Can also be specified via environment variable `TERMS_OF_SERVICE_URL`.
84
84
+
#terms_of_service_url =
85
85
+
86
86
+
# Operator contact email address.
87
87
+
#
88
88
+
# Can also be specified via environment variable `CONTACT_EMAIL`.
89
89
+
#contact_email =
90
90
+
91
91
+
# Maximum allowed blob size in bytes (default 10 GiB).
92
92
+
#
93
93
+
# Can also be specified via environment variable `MAX_BLOB_SIZE`.
94
94
+
#
95
95
+
# Default value: 10737418240
96
96
+
#max_blob_size = 10737418240
97
97
+
98
98
+
[database]
99
99
+
# PostgreSQL connection URL.
100
100
+
#
101
101
+
# Can also be specified via environment variable `DATABASE_URL`.
102
102
+
#
103
103
+
# Required! This value must be specified.
104
104
+
#url =
105
105
+
106
106
+
# Maximum number of connections in the pool.
107
107
+
#
108
108
+
# Can also be specified via environment variable `DATABASE_MAX_CONNECTIONS`.
109
109
+
#
110
110
+
# Default value: 100
111
111
+
#max_connections = 100
112
112
+
113
113
+
# Minimum number of idle connections kept in the pool.
114
114
+
#
115
115
+
# Can also be specified via environment variable `DATABASE_MIN_CONNECTIONS`.
116
116
+
#
117
117
+
# Default value: 10
118
118
+
#min_connections = 10
119
119
+
120
120
+
# Timeout in seconds when acquiring a connection from the pool.
121
121
+
#
122
122
+
# Can also be specified via environment variable `DATABASE_ACQUIRE_TIMEOUT_SECS`.
123
123
+
#
124
124
+
# Default value: 10
125
125
+
#acquire_timeout_secs = 10
126
126
+
127
127
+
[secrets]
128
128
+
# Secret used for signing JWTs. Must be at least 32 characters in
129
129
+
# production.
130
130
+
#
131
131
+
# Can also be specified via environment variable `JWT_SECRET`.
132
132
+
#jwt_secret =
133
133
+
134
134
+
# Secret used for DPoP proof validation. Must be at least 32 characters
135
135
+
# in production.
136
136
+
#
137
137
+
# Can also be specified via environment variable `DPOP_SECRET`.
138
138
+
#dpop_secret =
139
139
+
140
140
+
# Master key used for key-encryption and HKDF derivation. Must be at
141
141
+
# least 32 characters in production.
142
142
+
#
143
143
+
# Can also be specified via environment variable `MASTER_KEY`.
144
144
+
#master_key =
145
145
+
146
146
+
# PLC rotation key (DID key). If not set, user-level keys are used.
147
147
+
#
148
148
+
# Can also be specified via environment variable `PLC_ROTATION_KEY`.
149
149
+
#plc_rotation_key =
150
150
+
151
151
+
# Allow insecure/test secrets. NEVER enable in production.
152
152
+
#
153
153
+
# Can also be specified via environment variable `TRANQUIL_PDS_ALLOW_INSECURE_SECRETS`.
154
154
+
#
155
155
+
# Default value: false
156
156
+
#allow_insecure = false
157
157
+
158
158
+
[storage]
159
159
+
# Storage backend: `filesystem` or `s3`.
160
160
+
#
161
161
+
# Can also be specified via environment variable `BLOB_STORAGE_BACKEND`.
162
162
+
#
163
163
+
# Default value: "filesystem"
164
164
+
#backend = "filesystem"
165
165
+
166
166
+
# Path on disk for the filesystem blob backend.
167
167
+
#
168
168
+
# Can also be specified via environment variable `BLOB_STORAGE_PATH`.
169
169
+
#
170
170
+
# Default value: "/var/lib/tranquil-pds/blobs"
171
171
+
#path = "/var/lib/tranquil-pds/blobs"
172
172
+
173
173
+
# S3 bucket name for blob storage.
174
174
+
#
175
175
+
# Can also be specified via environment variable `S3_BUCKET`.
176
176
+
#s3_bucket =
177
177
+
178
178
+
# Custom S3 endpoint URL (for MinIO, R2, etc.).
179
179
+
#
180
180
+
# Can also be specified via environment variable `S3_ENDPOINT`.
181
181
+
#s3_endpoint =
182
182
+
183
183
+
[backup]
184
184
+
# Enable automatic backups.
185
185
+
#
186
186
+
# Can also be specified via environment variable `BACKUP_ENABLED`.
187
187
+
#
188
188
+
# Default value: true
189
189
+
#enabled = true
190
190
+
191
191
+
# Backup storage backend: `filesystem` or `s3`.
192
192
+
#
193
193
+
# Can also be specified via environment variable `BACKUP_STORAGE_BACKEND`.
194
194
+
#
195
195
+
# Default value: "filesystem"
196
196
+
#backend = "filesystem"
197
197
+
198
198
+
# Path on disk for the filesystem backup backend.
199
199
+
#
200
200
+
# Can also be specified via environment variable `BACKUP_STORAGE_PATH`.
201
201
+
#
202
202
+
# Default value: "/var/lib/tranquil-pds/backups"
203
203
+
#path = "/var/lib/tranquil-pds/backups"
204
204
+
205
205
+
# S3 bucket name for backups.
206
206
+
#
207
207
+
# Can also be specified via environment variable `BACKUP_S3_BUCKET`.
208
208
+
#s3_bucket =
209
209
+
210
210
+
# Number of backup revisions to keep per account.
211
211
+
#
212
212
+
# Can also be specified via environment variable `BACKUP_RETENTION_COUNT`.
213
213
+
#
214
214
+
# Default value: 7
215
215
+
#retention_count = 7
216
216
+
217
217
+
# Seconds between backup runs.
218
218
+
#
219
219
+
# Can also be specified via environment variable `BACKUP_INTERVAL_SECS`.
220
220
+
#
221
221
+
# Default value: 86400
222
222
+
#interval_secs = 86400
223
223
+
224
224
+
[cache]
225
225
+
# Cache backend: `ripple` (default, built-in gossip) or `valkey`.
226
226
+
#
227
227
+
# Can also be specified via environment variable `CACHE_BACKEND`.
228
228
+
#
229
229
+
# Default value: "ripple"
230
230
+
#backend = "ripple"
231
231
+
232
232
+
# Valkey / Redis connection URL. Required when `backend = "valkey"`.
233
233
+
#
234
234
+
# Can also be specified via environment variable `VALKEY_URL`.
235
235
+
#valkey_url =
236
236
+
237
237
+
[cache.ripple]
238
238
+
# Address to bind the Ripple gossip protocol listener.
239
239
+
#
240
240
+
# Can also be specified via environment variable `RIPPLE_BIND`.
241
241
+
#
242
242
+
# Default value: "0.0.0.0:0"
243
243
+
#bind_addr = "0.0.0.0:0"
244
244
+
245
245
+
# List of seed peer addresses.
246
246
+
#
247
247
+
# Can also be specified via environment variable `RIPPLE_PEERS`.
248
248
+
#peers =
249
249
+
250
250
+
# Unique machine identifier. Auto-derived from hostname when not set.
251
251
+
#
252
252
+
# Can also be specified via environment variable `RIPPLE_MACHINE_ID`.
253
253
+
#machine_id =
254
254
+
255
255
+
# Gossip protocol interval in milliseconds.
256
256
+
#
257
257
+
# Can also be specified via environment variable `RIPPLE_GOSSIP_INTERVAL_MS`.
258
258
+
#
259
259
+
# Default value: 200
260
260
+
#gossip_interval_ms = 200
261
261
+
262
262
+
# Maximum cache size in megabytes.
263
263
+
#
264
264
+
# Can also be specified via environment variable `RIPPLE_CACHE_MAX_MB`.
265
265
+
#
266
266
+
# Default value: 256
267
267
+
#cache_max_mb = 256
268
268
+
269
269
+
[plc]
270
270
+
# Base URL of the PLC directory.
271
271
+
#
272
272
+
# Can also be specified via environment variable `PLC_DIRECTORY_URL`.
273
273
+
#
274
274
+
# Default value: "https://plc.directory"
275
275
+
#directory_url = "https://plc.directory"
276
276
+
277
277
+
# HTTP request timeout in seconds.
278
278
+
#
279
279
+
# Can also be specified via environment variable `PLC_TIMEOUT_SECS`.
280
280
+
#
281
281
+
# Default value: 10
282
282
+
#timeout_secs = 10
283
283
+
284
284
+
# TCP connect timeout in seconds.
285
285
+
#
286
286
+
# Can also be specified via environment variable `PLC_CONNECT_TIMEOUT_SECS`.
287
287
+
#
288
288
+
# Default value: 5
289
289
+
#connect_timeout_secs = 5
290
290
+
291
291
+
# Seconds to cache DID documents in memory.
292
292
+
#
293
293
+
# Can also be specified via environment variable `DID_CACHE_TTL_SECS`.
294
294
+
#
295
295
+
# Default value: 300
296
296
+
#did_cache_ttl_secs = 300
297
297
+
298
298
+
[firehose]
299
299
+
# Size of the in-memory broadcast buffer for firehose events.
300
300
+
#
301
301
+
# Can also be specified via environment variable `FIREHOSE_BUFFER_SIZE`.
302
302
+
#
303
303
+
# Default value: 10000
304
304
+
#buffer_size = 10000
305
305
+
306
306
+
# How many hours of historical events to replay for cursor-based
307
307
+
# firehose connections.
308
308
+
#
309
309
+
# Can also be specified via environment variable `FIREHOSE_BACKFILL_HOURS`.
310
310
+
#
311
311
+
# Default value: 72
312
312
+
#backfill_hours = 72
313
313
+
314
314
+
# Maximum number of lagged events before disconnecting a slow consumer.
315
315
+
#
316
316
+
# Can also be specified via environment variable `FIREHOSE_MAX_LAG`.
317
317
+
#
318
318
+
# Default value: 5000
319
319
+
#max_lag = 5000
320
320
+
321
321
+
# List of relay / crawler notification URLs.
322
322
+
#
323
323
+
# Can also be specified via environment variable `CRAWLERS`.
324
324
+
#crawlers =
325
325
+
326
326
+
[email]
327
327
+
# Sender email address. When unset, email sending is disabled.
328
328
+
#
329
329
+
# Can also be specified via environment variable `MAIL_FROM_ADDRESS`.
330
330
+
#from_address =
331
331
+
332
332
+
# Display name used in the `From` header.
333
333
+
#
334
334
+
# Can also be specified via environment variable `MAIL_FROM_NAME`.
335
335
+
#
336
336
+
# Default value: "Tranquil PDS"
337
337
+
#from_name = "Tranquil PDS"
338
338
+
339
339
+
# Path to the `sendmail` binary.
340
340
+
#
341
341
+
# Can also be specified via environment variable `SENDMAIL_PATH`.
342
342
+
#
343
343
+
# Default value: "/usr/sbin/sendmail"
344
344
+
#sendmail_path = "/usr/sbin/sendmail"
345
345
+
346
346
+
[discord]
347
347
+
# Discord bot token. When unset, Discord integration is disabled.
348
348
+
#
349
349
+
# Can also be specified via environment variable `DISCORD_BOT_TOKEN`.
350
350
+
#bot_token =
351
351
+
352
352
+
[telegram]
353
353
+
# Telegram bot token. When unset, Telegram integration is disabled.
354
354
+
#
355
355
+
# Can also be specified via environment variable `TELEGRAM_BOT_TOKEN`.
356
356
+
#bot_token =
357
357
+
358
358
+
# Secret token for incoming webhook verification.
359
359
+
#
360
360
+
# Can also be specified via environment variable `TELEGRAM_WEBHOOK_SECRET`.
361
361
+
#webhook_secret =
362
362
+
363
363
+
[signal]
364
364
+
# Path to the `signal-cli` binary.
365
365
+
#
366
366
+
# Can also be specified via environment variable `SIGNAL_CLI_PATH`.
367
367
+
#
368
368
+
# Default value: "/usr/local/bin/signal-cli"
369
369
+
#cli_path = "/usr/local/bin/signal-cli"
370
370
+
371
371
+
# Sender phone number. When unset, Signal integration is disabled.
372
372
+
#
373
373
+
# Can also be specified via environment variable `SIGNAL_SENDER_NUMBER`.
374
374
+
#sender_number =
375
375
+
376
376
+
[notifications]
377
377
+
# Polling interval in milliseconds for the comms queue.
378
378
+
#
379
379
+
# Can also be specified via environment variable `NOTIFICATION_POLL_INTERVAL_MS`.
380
380
+
#
381
381
+
# Default value: 1000
382
382
+
#poll_interval_ms = 1000
383
383
+
384
384
+
# Number of notifications to process per batch.
385
385
+
#
386
386
+
# Can also be specified via environment variable `NOTIFICATION_BATCH_SIZE`.
387
387
+
#
388
388
+
# Default value: 100
389
389
+
#batch_size = 100
390
390
+
391
391
+
[sso]
392
392
+
[sso.github]
393
393
+
# Default value: false
394
394
+
#enabled = false
395
395
+
396
396
+
#client_id =
397
397
+
398
398
+
#client_secret =
399
399
+
400
400
+
#display_name =
401
401
+
402
402
+
[sso.discord]
403
403
+
# Default value: false
404
404
+
#enabled = false
405
405
+
406
406
+
#client_id =
407
407
+
408
408
+
#client_secret =
409
409
+
410
410
+
#display_name =
411
411
+
412
412
+
[sso.google]
413
413
+
# Default value: false
414
414
+
#enabled = false
415
415
+
416
416
+
#client_id =
417
417
+
418
418
+
#client_secret =
419
419
+
420
420
+
#display_name =
421
421
+
422
422
+
[sso.gitlab]
423
423
+
# Default value: false
424
424
+
#enabled = false
425
425
+
426
426
+
#client_id =
427
427
+
428
428
+
#client_secret =
429
429
+
430
430
+
#issuer =
431
431
+
432
432
+
#display_name =
433
433
+
434
434
+
[sso.oidc]
435
435
+
# Default value: false
436
436
+
#enabled = false
437
437
+
438
438
+
#client_id =
439
439
+
440
440
+
#client_secret =
441
441
+
442
442
+
#issuer =
443
443
+
444
444
+
#display_name =
445
445
+
446
446
+
[sso.apple]
447
447
+
# Can also be specified via environment variable `SSO_APPLE_ENABLED`.
448
448
+
# Default value: false
449
449
+
#enabled = false
450
450
+
451
451
+
# Can also be specified via environment variable `SSO_APPLE_CLIENT_ID`.
452
452
+
#client_id =
453
453
+
454
454
+
# Can also be specified via environment variable `SSO_APPLE_TEAM_ID`.
455
455
+
#team_id =
456
456
+
457
457
+
# Can also be specified via environment variable `SSO_APPLE_KEY_ID`.
458
458
+
#key_id =
459
459
+
460
460
+
# Can also be specified via environment variable `SSO_APPLE_PRIVATE_KEY`.
461
461
+
#private_key =
462
462
+
463
463
+
[moderation]
464
464
+
# External report-handling service URL.
465
465
+
#
466
466
+
# Can also be specified via environment variable `REPORT_SERVICE_URL`.
467
467
+
#report_service_url =
468
468
+
469
469
+
# DID of the external report-handling service.
470
470
+
#
471
471
+
# Can also be specified via environment variable `REPORT_SERVICE_DID`.
472
472
+
#report_service_did =
473
473
+
474
474
+
[import]
475
475
+
# Whether the PDS accepts repo imports.
476
476
+
#
477
477
+
# Can also be specified via environment variable `ACCEPTING_REPO_IMPORTS`.
478
478
+
#
479
479
+
# Default value: true
480
480
+
#accepting = true
481
481
+
482
482
+
# Maximum allowed import archive size in bytes (default 1 GiB).
483
483
+
#
484
484
+
# Can also be specified via environment variable `MAX_IMPORT_SIZE`.
485
485
+
#
486
486
+
# Default value: 1073741824
487
487
+
#max_size = 1073741824
488
488
+
489
489
+
# Maximum number of blocks allowed in an import.
490
490
+
#
491
491
+
# Can also be specified via environment variable `MAX_IMPORT_BLOCKS`.
492
492
+
#
493
493
+
# Default value: 500000
494
494
+
#max_blocks = 500000
495
495
+
496
496
+
# Skip CAR verification during import. Only for development/debugging.
497
497
+
#
498
498
+
# Can also be specified via environment variable `SKIP_IMPORT_VERIFICATION`.
499
499
+
#
500
500
+
# Default value: false
501
501
+
#skip_verification = false
502
502
+
503
503
+
[scheduled]
504
504
+
# Interval in seconds between scheduled delete checks.
505
505
+
#
506
506
+
# Can also be specified via environment variable `SCHEDULED_DELETE_CHECK_INTERVAL_SECS`.
507
507
+
#
508
508
+
# Default value: 3600
509
509
+
#delete_check_interval_secs = 3600