1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::net::{IpAddr, SocketAddr};
7use std::path::Path;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct Config {
11 #[serde(default)]
12 pub server: ServerConfig,
13 #[serde(default)]
14 pub storage: StorageConfig,
15 #[serde(default)]
16 pub nostr: NostrConfig,
17 #[serde(default)]
18 pub blossom: BlossomConfig,
19 #[serde(default)]
20 pub sync: SyncConfig,
21 #[serde(default)]
22 pub cashu: CashuConfig,
23 #[serde(default)]
24 pub updater: UpdaterConfig,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct UpdaterConfig {
34 #[serde(default = "default_auto_check")]
35 pub auto_check: bool,
36 #[serde(default)]
37 pub auto_install: bool,
38 #[serde(default = "default_check_interval_hours")]
39 pub check_interval_hours: u32,
40}
41
42impl Default for UpdaterConfig {
43 fn default() -> Self {
44 Self {
45 auto_check: default_auto_check(),
46 auto_install: false,
47 check_interval_hours: default_check_interval_hours(),
48 }
49 }
50}
51
52fn default_auto_check() -> bool {
53 true
54}
55
56fn default_check_interval_hours() -> u32 {
57 24
58}
59
60#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum ServerMode {
63 #[default]
64 Normal,
65 #[serde(alias = "signal-only")]
66 Assist,
67}
68
69impl ServerMode {
70 pub const fn as_str(self) -> &'static str {
71 match self {
72 Self::Normal => "normal",
73 Self::Assist => "assist",
74 }
75 }
76
77 pub const fn hash_get_enabled(self) -> bool {
78 matches!(self, Self::Normal)
79 }
80
81 pub const fn background_services_enabled(self) -> bool {
82 matches!(self, Self::Normal)
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ServerConfig {
88 #[serde(default)]
89 pub mode: ServerMode,
90 #[serde(default = "default_bind_address")]
91 pub bind_address: String,
92 #[serde(default = "default_enable_auth")]
93 pub enable_auth: bool,
94 #[serde(default = "default_enable_fips")]
96 pub enable_fips: bool,
97 #[serde(default = "default_fips_discovery_scope")]
99 pub fips_discovery_scope: String,
100 #[serde(default)]
103 pub fips_open_discovery_max_pending: usize,
104 #[serde(default)]
107 pub fips_local_rendezvous_addr: Option<String>,
108 #[serde(default = "default_enable_fips_lan_discovery")]
110 pub enable_fips_lan_discovery: bool,
111 #[serde(default)]
115 pub fips_relays: Option<Vec<String>>,
116 #[serde(
119 default,
120 alias = "preconfigured_fips_peers",
121 alias = "fips_static_peers"
122 )]
123 pub fips_peers: Vec<ConfiguredFipsPeer>,
124 #[serde(default = "default_enable_fips_udp")]
126 pub enable_fips_udp: bool,
127 #[serde(default)]
129 pub fips_udp_bind_addr: Option<String>,
130 #[serde(default)]
132 pub fips_udp_public: bool,
133 #[serde(default)]
135 pub fips_udp_external_addr: Option<String>,
136 #[serde(default = "default_enable_fips_webrtc")]
138 pub enable_fips_webrtc: bool,
139 #[serde(default)]
141 pub fips_ethernet_interfaces: Vec<String>,
142 #[serde(default = "default_fetch_from_fips_peers", alias = "http_fips_fetch")]
144 pub fetch_from_fips_peers: bool,
145 #[serde(default = "default_fips_request_timeout_ms")]
147 pub fips_request_timeout_ms: u64,
148 #[serde(default = "default_public_writes")]
151 pub public_writes: bool,
152 #[serde(default = "default_public_plaintext_reads")]
155 pub public_plaintext_reads: bool,
156 #[serde(default = "default_socialgraph_snapshot_public")]
158 pub socialgraph_snapshot_public: bool,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ConfiguredFipsPeer {
163 pub npub: String,
164 #[serde(default)]
165 pub udp_addresses: Vec<String>,
166}
167
168fn default_public_writes() -> bool {
169 true
170}
171
172fn default_public_plaintext_reads() -> bool {
173 false
174}
175
176fn default_socialgraph_snapshot_public() -> bool {
177 false
178}
179
180impl ServerConfig {
181 pub fn resolved_fips_relays(&self, active_nostr_relays: &[String]) -> Vec<String> {
182 match &self.fips_relays {
183 Some(relays) => normalize_fips_signal_relays(relays),
187 None => merge_fips_signal_relays(active_nostr_relays),
188 }
189 }
190}
191
192const DEFAULT_FIPS_SIGNAL_RELAYS: [&str; 2] = ["wss://temp.iris.to", "wss://relay.primal.net"];
193
194fn merge_fips_signal_relays(configured: &[String]) -> Vec<String> {
195 normalize_fips_signal_relays(
196 &configured
197 .iter()
198 .cloned()
199 .chain(DEFAULT_FIPS_SIGNAL_RELAYS.into_iter().map(str::to_string))
200 .collect::<Vec<_>>(),
201 )
202}
203
204fn normalize_fips_signal_relays(configured: &[String]) -> Vec<String> {
205 let mut relays = Vec::new();
206 for relay in configured {
207 let normalized = relay.trim().trim_end_matches('/').to_string();
208 if normalized.is_empty() || relays.contains(&normalized) {
209 continue;
210 }
211 relays.push(normalized);
212 }
213 relays
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct StorageConfig {
218 #[serde(default = "default_data_dir")]
219 pub data_dir: String,
220 #[serde(default = "default_max_size_gb")]
221 pub max_size_gb: u64,
222 #[serde(default = "default_storage_evict_orphans")]
223 pub evict_orphans: bool,
224 #[serde(default)]
226 pub s3: Option<S3Config>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct S3Config {
232 pub endpoint: String,
234 pub bucket: String,
236 #[serde(default)]
238 pub prefix: Option<String>,
239 #[serde(default = "default_s3_region")]
241 pub region: String,
242 #[serde(default)]
244 pub access_key: Option<String>,
245 #[serde(default)]
247 pub secret_key: Option<String>,
248 #[serde(default)]
250 pub public_url: Option<String>,
251}
252
253fn default_s3_region() -> String {
254 "auto".to_string()
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
258#[serde(rename_all = "kebab-case")]
259pub enum NostrEventTransport {
260 #[default]
261 Relay,
262 FipsLocalOnly,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct NostrConfig {
267 #[serde(default = "default_nostr_enabled")]
268 pub enabled: bool,
269 #[serde(default = "default_relays")]
270 pub relays: Vec<String>,
271 #[serde(default)]
273 pub event_transport: NostrEventTransport,
274 #[serde(default)]
276 pub allowed_npubs: Vec<String>,
277 #[serde(default)]
279 pub socialgraph_root: Option<String>,
280 #[serde(default = "default_nostr_bootstrap_follows")]
283 pub bootstrap_follows: Vec<String>,
284 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
286 pub social_graph_crawl_depth: u32,
287 #[serde(default)]
290 pub mirror_max_follow_distance: Option<u32>,
291 #[serde(default = "default_max_write_distance")]
293 pub max_write_distance: u32,
294 #[serde(default = "default_nostr_db_max_size_gb")]
296 pub db_max_size_gb: u64,
297 #[serde(default = "default_nostr_spambox_max_size_gb")]
300 pub spambox_max_size_gb: u64,
301 #[serde(default)]
303 pub negentropy_only: bool,
304 #[serde(default = "default_nostr_overmute_threshold")]
306 pub overmute_threshold: f64,
307 #[serde(default = "default_nostr_mirror_kinds")]
309 pub mirror_kinds: Vec<u16>,
310 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
313 pub history_sync_author_chunk_size: usize,
314 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
316 pub history_sync_per_author_event_limit: usize,
317 #[serde(default = "default_nostr_history_sync_on_reconnect")]
319 pub history_sync_on_reconnect: bool,
320 #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
323 pub full_text_note_history_follow_distance: Option<u32>,
324 #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
327 pub full_text_note_history_max_relay_pages: usize,
328 #[serde(default = "default_nostr_archive_history_follow_distance")]
333 pub archive_history_follow_distance: Option<u32>,
334 #[serde(default = "default_nostr_archive_history_max_relay_pages")]
337 pub archive_history_max_relay_pages: usize,
338 #[serde(default, alias = "relayless_pubsub")]
341 pub decentralized_pubsub: bool,
342 #[serde(default = "default_nostr_decentralized_pubsub_max_event_bytes")]
345 pub decentralized_pubsub_max_event_bytes: usize,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct BlossomConfig {
350 #[serde(default = "default_blossom_enabled")]
351 pub enabled: bool,
352 #[serde(default)]
354 pub servers: Vec<String>,
355 #[serde(default = "default_read_servers")]
357 pub read_servers: Vec<String>,
358 #[serde(default = "default_write_servers")]
360 pub write_servers: Vec<String>,
361 #[serde(default = "default_max_upload_mb")]
363 pub max_upload_mb: u64,
364 #[serde(default = "default_require_random_untrusted_ingest")]
366 pub require_random_untrusted_ingest: bool,
367 #[serde(default = "default_optimistic_uploads")]
370 pub optimistic_uploads: bool,
371 #[serde(
375 default,
376 alias = "write_behind_servers",
377 alias = "mirror_write_servers"
378 )]
379 pub replicate_servers: Vec<String>,
380 #[serde(default = "default_replicate_queue_mb")]
382 pub replicate_queue_mb: u64,
383}
384
385impl BlossomConfig {
386 pub fn all_read_servers(&self) -> Vec<String> {
387 if !self.enabled {
388 return Vec::new();
389 }
390 let mut servers = self.servers.clone();
391 servers.extend(self.read_servers.clone());
392 servers.extend(self.write_servers.clone());
393 if servers.is_empty() {
394 servers = default_read_servers();
395 servers.extend(default_write_servers());
396 }
397 servers.sort();
398 servers.dedup();
399 servers
400 }
401
402 pub fn upstream_read_servers(&self, bind_address: &str) -> Vec<String> {
407 let Some((bind_host, bind_port)) = parse_bind_authority(bind_address) else {
408 return self.all_read_servers();
409 };
410
411 self.all_read_servers()
412 .into_iter()
413 .filter(|server| !is_bound_http_server(server, &bind_host, bind_port))
414 .collect()
415 }
416
417 pub fn all_write_servers(&self) -> Vec<String> {
418 if !self.enabled {
419 return Vec::new();
420 }
421 let mut servers = self.servers.clone();
422 servers.extend(self.write_servers.clone());
423 if servers.is_empty() {
424 servers = default_write_servers();
425 }
426 servers.sort();
427 servers.dedup();
428 servers
429 }
430}
431
432fn parse_bind_authority(bind_address: &str) -> Option<(String, u16)> {
433 if let Ok(address) = bind_address.parse::<SocketAddr>() {
434 return Some((address.ip().to_string(), address.port()));
435 }
436
437 let url = reqwest::Url::parse(&format!("http://{bind_address}")).ok()?;
438 Some((url.host_str()?.to_string(), url.port_or_known_default()?))
439}
440
441fn is_bound_http_server(server: &str, bind_host: &str, bind_port: u16) -> bool {
442 let Ok(url) = reqwest::Url::parse(server) else {
443 return false;
444 };
445 if url.scheme() != "http" || url.port_or_known_default() != Some(bind_port) {
446 return false;
447 }
448 let Some(upstream_host) = url.host_str() else {
449 return false;
450 };
451 let upstream_host = upstream_host
452 .strip_prefix('[')
453 .and_then(|host| host.strip_suffix(']'))
454 .unwrap_or(upstream_host);
455 if upstream_host.eq_ignore_ascii_case(bind_host) {
456 return true;
457 }
458
459 let bind_ip = bind_host.parse::<IpAddr>().ok();
460 let upstream_ip = upstream_host.parse::<IpAddr>().ok();
461 let upstream_is_localhost = upstream_host.eq_ignore_ascii_case("localhost");
462 match bind_ip {
463 Some(ip) if ip.is_unspecified() => {
464 upstream_is_localhost
465 || upstream_ip
466 .is_some_and(|candidate| candidate.is_loopback() || candidate.is_unspecified())
467 }
468 Some(ip) if ip.is_loopback() => {
469 upstream_is_localhost || upstream_ip.is_some_and(|candidate| candidate.is_loopback())
470 }
471 _ => false,
472 }
473}
474
475impl NostrConfig {
476 pub fn active_relays(&self) -> Vec<String> {
477 if self.enabled && self.event_transport == NostrEventTransport::Relay {
478 self.relays.clone()
479 } else {
480 Vec::new()
481 }
482 }
483
484 pub fn decentralized_pubsub_enabled(&self) -> bool {
485 self.enabled
486 && self.decentralized_pubsub
487 && cfg!(feature = "experimental-decentralized-pubsub")
488 }
489}
490
491fn default_nostr_decentralized_pubsub_max_event_bytes() -> usize {
492 nostr_pubsub_fips::FIPS_NOSTR_PUBSUB_MAX_DATAGRAM_BYTES
493}
494
495fn default_read_servers() -> Vec<String> {
497 let mut servers = vec![
498 "https://blossom.primal.net".to_string(),
499 "https://cdn.iris.to".to_string(),
500 ];
501 servers.sort();
502 servers
503}
504
505fn default_write_servers() -> Vec<String> {
506 vec!["https://upload.iris.to".to_string()]
507}
508
509fn default_max_upload_mb() -> u64 {
510 5
511}
512
513fn default_require_random_untrusted_ingest() -> bool {
514 true
515}
516
517fn default_optimistic_uploads() -> bool {
518 false
519}
520
521fn default_replicate_queue_mb() -> u64 {
522 256
523}
524
525fn default_nostr_enabled() -> bool {
526 true
527}
528
529fn default_blossom_enabled() -> bool {
530 true
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct SyncConfig {
535 #[serde(default = "default_sync_enabled")]
537 pub enabled: bool,
538 #[serde(default = "default_sync_own")]
540 pub sync_own: bool,
541 #[serde(default = "default_sync_followed")]
543 pub sync_followed: bool,
544 #[serde(default = "default_max_concurrent")]
546 pub max_concurrent: usize,
547 #[serde(default = "default_blossom_timeout_ms")]
549 pub blossom_timeout_ms: u64,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct CashuConfig {
554 #[serde(default)]
556 pub accepted_mints: Vec<String>,
557 #[serde(default)]
559 pub default_mint: Option<String>,
560 #[serde(default = "default_cashu_quote_payment_offer_sat")]
562 pub quote_payment_offer_sat: u64,
563 #[serde(default = "default_cashu_quote_ttl_ms")]
565 pub quote_ttl_ms: u32,
566 #[serde(default = "default_cashu_settlement_timeout_ms")]
568 pub settlement_timeout_ms: u64,
569 #[serde(default = "default_cashu_mint_failure_block_threshold")]
571 pub mint_failure_block_threshold: u64,
572 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
574 pub peer_suggested_mint_base_cap_sat: u64,
575 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
577 pub peer_suggested_mint_success_step_sat: u64,
578 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
580 pub peer_suggested_mint_receipt_step_sat: u64,
581 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
583 pub peer_suggested_mint_max_cap_sat: u64,
584 #[serde(default)]
586 pub payment_default_block_threshold: u64,
587 #[serde(default = "default_cashu_chunk_target_bytes")]
589 pub chunk_target_bytes: usize,
590}
591
592impl Default for CashuConfig {
593 fn default() -> Self {
594 Self {
595 accepted_mints: Vec::new(),
596 default_mint: None,
597 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
598 quote_ttl_ms: default_cashu_quote_ttl_ms(),
599 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
600 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
601 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
602 peer_suggested_mint_success_step_sat:
603 default_cashu_peer_suggested_mint_success_step_sat(),
604 peer_suggested_mint_receipt_step_sat:
605 default_cashu_peer_suggested_mint_receipt_step_sat(),
606 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
607 payment_default_block_threshold: 0,
608 chunk_target_bytes: default_cashu_chunk_target_bytes(),
609 }
610 }
611}
612
613fn default_cashu_quote_payment_offer_sat() -> u64 {
614 3
615}
616
617fn default_cashu_quote_ttl_ms() -> u32 {
618 1_500
619}
620
621fn default_cashu_settlement_timeout_ms() -> u64 {
622 5_000
623}
624
625fn default_cashu_mint_failure_block_threshold() -> u64 {
626 2
627}
628
629fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
630 3
631}
632
633fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
634 1
635}
636
637fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
638 2
639}
640
641fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
642 21
643}
644
645fn default_cashu_chunk_target_bytes() -> usize {
646 32 * 1024
647}
648
649fn default_sync_enabled() -> bool {
650 true
651}
652
653fn default_sync_own() -> bool {
654 true
655}
656
657fn default_sync_followed() -> bool {
658 true
659}
660
661fn default_max_concurrent() -> usize {
662 3
663}
664
665fn default_blossom_timeout_ms() -> u64 {
666 10000
667}
668
669fn default_social_graph_crawl_depth() -> u32 {
670 2
671}
672
673fn default_nostr_bootstrap_follows() -> Vec<String> {
674 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
675}
676
677fn default_max_write_distance() -> u32 {
678 3
679}
680
681fn default_nostr_db_max_size_gb() -> u64 {
682 10
683}
684
685fn default_nostr_spambox_max_size_gb() -> u64 {
686 1
687}
688
689fn default_nostr_history_sync_on_reconnect() -> bool {
690 true
691}
692
693fn default_nostr_overmute_threshold() -> f64 {
694 1.0
695}
696
697fn default_nostr_mirror_kinds() -> Vec<u16> {
698 vec![
699 0, 1, 3, 5, 6, 7, 16, 20, 1_111, 9_735, 10_000, 30_000, 30_023,
700 ]
701}
702
703fn default_nostr_history_sync_author_chunk_size() -> usize {
704 5_000
705}
706
707fn default_nostr_history_sync_per_author_event_limit() -> usize {
708 256
709}
710
711fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
712 Some(2)
713}
714
715fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
716 0
717}
718
719fn default_nostr_archive_history_follow_distance() -> Option<u32> {
720 Some(2)
721}
722
723fn default_nostr_archive_history_max_relay_pages() -> usize {
724 0
725}
726
727fn default_relays() -> Vec<String> {
728 vec![
729 "wss://nos.lol".to_string(),
730 "wss://relay.snort.social".to_string(),
731 "wss://temp.iris.to".to_string(),
732 ]
733}
734
735fn default_bind_address() -> String {
736 "127.0.0.1:8080".to_string()
737}
738
739fn default_enable_auth() -> bool {
740 true
741}
742
743fn default_enable_fips() -> bool {
744 true
745}
746
747fn default_enable_fips_lan_discovery() -> bool {
748 true
749}
750
751fn default_fips_discovery_scope() -> String {
752 hashtree_fips_transport::DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
753}
754
755fn default_enable_fips_udp() -> bool {
756 true
757}
758
759fn default_enable_fips_webrtc() -> bool {
760 cfg!(feature = "fips-webrtc")
761}
762
763fn default_fetch_from_fips_peers() -> bool {
764 true
765}
766
767fn default_fips_request_timeout_ms() -> u64 {
768 5_500
769}
770
771fn default_data_dir() -> String {
772 hashtree_config::get_hashtree_dir()
773 .join("data")
774 .to_string_lossy()
775 .to_string()
776}
777
778fn default_max_size_gb() -> u64 {
779 10
780}
781
782fn default_storage_evict_orphans() -> bool {
783 true
784}
785
786impl Default for ServerConfig {
787 fn default() -> Self {
788 Self {
789 mode: ServerMode::default(),
790 bind_address: default_bind_address(),
791 enable_auth: default_enable_auth(),
792 enable_fips: default_enable_fips(),
793 fips_discovery_scope: default_fips_discovery_scope(),
794 fips_open_discovery_max_pending: 0,
795 fips_local_rendezvous_addr: None,
796 enable_fips_lan_discovery: default_enable_fips_lan_discovery(),
797 fips_relays: None,
798 fips_peers: Vec::new(),
799 enable_fips_udp: default_enable_fips_udp(),
800 fips_udp_bind_addr: None,
801 fips_udp_public: false,
802 fips_udp_external_addr: None,
803 enable_fips_webrtc: default_enable_fips_webrtc(),
804 fips_ethernet_interfaces: Vec::new(),
805 fetch_from_fips_peers: default_fetch_from_fips_peers(),
806 fips_request_timeout_ms: default_fips_request_timeout_ms(),
807 public_writes: default_public_writes(),
808 public_plaintext_reads: default_public_plaintext_reads(),
809 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
810 }
811 }
812}
813
814impl Default for StorageConfig {
815 fn default() -> Self {
816 Self {
817 data_dir: default_data_dir(),
818 max_size_gb: default_max_size_gb(),
819 evict_orphans: default_storage_evict_orphans(),
820 s3: None,
821 }
822 }
823}
824
825impl Default for NostrConfig {
826 fn default() -> Self {
827 Self {
828 enabled: default_nostr_enabled(),
829 relays: default_relays(),
830 event_transport: NostrEventTransport::default(),
831 allowed_npubs: Vec::new(),
832 socialgraph_root: None,
833 bootstrap_follows: default_nostr_bootstrap_follows(),
834 social_graph_crawl_depth: default_social_graph_crawl_depth(),
835 mirror_max_follow_distance: None,
836 max_write_distance: default_max_write_distance(),
837 db_max_size_gb: default_nostr_db_max_size_gb(),
838 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
839 negentropy_only: false,
840 overmute_threshold: default_nostr_overmute_threshold(),
841 mirror_kinds: default_nostr_mirror_kinds(),
842 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
843 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
844 ),
845 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
846 full_text_note_history_follow_distance:
847 default_nostr_full_text_note_history_follow_distance(),
848 full_text_note_history_max_relay_pages:
849 default_nostr_full_text_note_history_max_relay_pages(),
850 archive_history_follow_distance: default_nostr_archive_history_follow_distance(),
851 archive_history_max_relay_pages: default_nostr_archive_history_max_relay_pages(),
852 decentralized_pubsub: false,
853 decentralized_pubsub_max_event_bytes:
854 default_nostr_decentralized_pubsub_max_event_bytes(),
855 }
856 }
857}
858
859impl Default for BlossomConfig {
860 fn default() -> Self {
861 Self {
862 enabled: default_blossom_enabled(),
863 servers: Vec::new(),
864 read_servers: default_read_servers(),
865 write_servers: default_write_servers(),
866 max_upload_mb: default_max_upload_mb(),
867 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
868 optimistic_uploads: default_optimistic_uploads(),
869 replicate_servers: Vec::new(),
870 replicate_queue_mb: default_replicate_queue_mb(),
871 }
872 }
873}
874
875impl Default for SyncConfig {
876 fn default() -> Self {
877 Self {
878 enabled: default_sync_enabled(),
879 sync_own: default_sync_own(),
880 sync_followed: default_sync_followed(),
881 max_concurrent: default_max_concurrent(),
882 blossom_timeout_ms: default_blossom_timeout_ms(),
883 }
884 }
885}
886
887impl Config {
888 pub fn load() -> Result<Self> {
890 let config_path = get_config_path();
891
892 if config_path.exists() {
893 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
894 toml::from_str(&content).context("Failed to parse config file")
895 } else {
896 let config = Config::default();
897 config.save()?;
898 Ok(config)
899 }
900 }
901
902 pub fn save(&self) -> Result<()> {
904 let config_path = get_config_path();
905
906 if let Some(parent) = config_path.parent() {
908 fs::create_dir_all(parent)?;
909 }
910
911 let content = toml::to_string_pretty(self)?;
912 fs::write(&config_path, content)?;
913
914 Ok(())
915 }
916}
917
918pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
920
921fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
922 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
923 let entries = hashtree_config::parse_keys_file(&content);
924 let nsec_str = entries
925 .into_iter()
926 .next()
927 .map(|e| e.secret)
928 .context("Keys file is empty")?;
929 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
930 Ok(Keys::new(secret_key))
931}
932
933fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
934 if let (Some(data_dir), Some(config)) = (data_dir, config) {
935 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
936 }
937}
938
939fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
940 if let Some(parent) = keys_path.parent() {
941 fs::create_dir_all(parent)?;
942 }
943
944 let nsec = keys
945 .secret_key()
946 .to_bech32()
947 .context("Failed to encode nsec")?;
948 fs::write(keys_path, &nsec)?;
949
950 #[cfg(unix)]
951 {
952 use std::os::unix::fs::PermissionsExt;
953 let perms = fs::Permissions::from_mode(0o600);
954 fs::set_permissions(keys_path, perms)?;
955 }
956
957 Ok(())
958}
959
960pub fn ensure_auth_cookie() -> Result<(String, String)> {
962 let cookie_path = get_auth_cookie_path();
963
964 if cookie_path.exists() {
965 read_auth_cookie()
966 } else {
967 generate_auth_cookie()
968 }
969}
970
971pub fn read_auth_cookie() -> Result<(String, String)> {
973 let cookie_path = get_auth_cookie_path();
974 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
975
976 let parts: Vec<&str> = content.trim().split(':').collect();
977 if parts.len() != 2 {
978 anyhow::bail!("Invalid auth cookie format");
979 }
980
981 Ok((parts[0].to_string(), parts[1].to_string()))
982}
983
984pub fn ensure_keys() -> Result<(Keys, bool)> {
987 let config_dir = get_hashtree_dir();
988 let config = Config::load().ok();
989 let data_dir = config
990 .as_ref()
991 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
992 ensure_keys_in(&config_dir, data_dir, config.as_ref())
993}
994
995pub fn ensure_keys_in(
998 config_dir: &Path,
999 data_dir: Option<&Path>,
1000 config: Option<&Config>,
1001) -> Result<(Keys, bool)> {
1002 let keys_path = config_dir.join("keys");
1003
1004 if keys_path.exists() {
1005 Ok((read_keys_from_path(&keys_path)?, false))
1006 } else {
1007 let keys = generate_keys_in(config_dir, data_dir, config)?;
1008 Ok((keys, true))
1009 }
1010}
1011
1012pub fn read_keys() -> Result<Keys> {
1014 read_keys_in(&get_hashtree_dir())
1015}
1016
1017pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
1019 read_keys_from_path(&config_dir.join("keys"))
1020}
1021
1022pub fn ensure_keys_string() -> Result<(String, bool)> {
1025 let config_dir = get_hashtree_dir();
1026 let config = Config::load().ok();
1027 let data_dir = config
1028 .as_ref()
1029 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
1030 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
1031}
1032
1033pub fn ensure_keys_string_in(
1036 config_dir: &Path,
1037 data_dir: Option<&Path>,
1038 config: Option<&Config>,
1039) -> Result<(String, bool)> {
1040 let keys_path = config_dir.join("keys");
1041
1042 if keys_path.exists() {
1043 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
1044 let entries = hashtree_config::parse_keys_file(&content);
1045 let nsec_str = entries
1046 .into_iter()
1047 .next()
1048 .map(|e| e.secret)
1049 .context("Keys file is empty")?;
1050 Ok((nsec_str, false))
1051 } else {
1052 let keys = generate_keys_in(config_dir, data_dir, config)?;
1053 let nsec = keys
1054 .secret_key()
1055 .to_bech32()
1056 .context("Failed to encode nsec")?;
1057 Ok((nsec, true))
1058 }
1059}
1060
1061pub fn generate_keys() -> Result<Keys> {
1063 let config_dir = get_hashtree_dir();
1064 let config = Config::load().ok();
1065 let data_dir = config
1066 .as_ref()
1067 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
1068 generate_keys_in(&config_dir, data_dir, config.as_ref())
1069}
1070
1071pub fn generate_keys_in(
1074 config_dir: &Path,
1075 data_dir: Option<&Path>,
1076 config: Option<&Config>,
1077) -> Result<Keys> {
1078 let keys = Keys::generate();
1079 write_keys_to_path(&config_dir.join("keys"), &keys)?;
1080 seed_identity_defaults_if_needed(data_dir, config);
1081 Ok(keys)
1082}
1083
1084pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
1086 keys.public_key().to_bytes()
1087}
1088
1089pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
1091 use nostr::PublicKey;
1092 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
1093 Ok(pk.to_bytes())
1094}
1095
1096pub fn generate_auth_cookie() -> Result<(String, String)> {
1098 use rand::Rng;
1099
1100 let cookie_path = get_auth_cookie_path();
1101
1102 if let Some(parent) = cookie_path.parent() {
1104 fs::create_dir_all(parent)?;
1105 }
1106
1107 let mut rng = rand::thread_rng();
1109 let username = format!("htree_{}", rng.gen::<u32>());
1110 let password: String = (0..32)
1111 .map(|_| {
1112 let idx = rng.gen_range(0..62);
1113 match idx {
1114 0..=25 => (b'a' + idx) as char,
1115 26..=51 => (b'A' + (idx - 26)) as char,
1116 _ => (b'0' + (idx - 52)) as char,
1117 }
1118 })
1119 .collect();
1120
1121 let content = format!("{}:{}", username, password);
1123 fs::write(&cookie_path, content)?;
1124
1125 #[cfg(unix)]
1127 {
1128 use std::os::unix::fs::PermissionsExt;
1129 let perms = fs::Permissions::from_mode(0o600);
1130 fs::set_permissions(&cookie_path, perms)?;
1131 }
1132
1133 Ok((username, password))
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139 use crate::test_support::{test_env_lock, EnvVarGuard};
1140 use tempfile::TempDir;
1141
1142 #[test]
1143 fn test_config_default() {
1144 let config = Config::default();
1145 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1146 assert!(config.server.enable_auth);
1147 assert!(!config.server.public_plaintext_reads);
1148 assert_eq!(config.storage.max_size_gb, 10);
1149 assert!(config.storage.evict_orphans);
1150 assert!(config.nostr.enabled);
1151 assert!(config
1152 .nostr
1153 .relays
1154 .contains(&"wss://temp.iris.to".to_string()));
1155 assert!(config.blossom.enabled);
1156 assert!(!config.blossom.optimistic_uploads);
1157 assert!(config.blossom.replicate_servers.is_empty());
1158 assert_eq!(config.blossom.replicate_queue_mb, 256);
1159 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1160 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1161 assert_eq!(config.nostr.max_write_distance, 3);
1162 assert_eq!(config.nostr.db_max_size_gb, 10);
1163 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1164 assert!(!config.nostr.negentropy_only);
1165 assert_eq!(config.nostr.overmute_threshold, 1.0);
1166 assert_eq!(
1167 config.nostr.mirror_kinds,
1168 vec![0, 1, 3, 5, 6, 7, 16, 20, 1_111, 9_735, 10_000, 30_000, 30_023]
1169 );
1170 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1171 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1172 assert!(config.nostr.history_sync_on_reconnect);
1173 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1174 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1175 assert_eq!(config.nostr.archive_history_follow_distance, Some(2));
1176 assert_eq!(config.nostr.archive_history_max_relay_pages, 0);
1177 assert!(config.nostr.socialgraph_root.is_none());
1178 assert_eq!(
1179 config.nostr.bootstrap_follows,
1180 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1181 );
1182 assert!(!config.server.socialgraph_snapshot_public);
1183 assert!(config.cashu.accepted_mints.is_empty());
1184 assert!(config.cashu.default_mint.is_none());
1185 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1186 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1187 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1188 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1189 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1190 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1191 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1192 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1193 assert_eq!(config.cashu.payment_default_block_threshold, 0);
1194 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1195 }
1196
1197 #[test]
1198 fn test_blossom_optimistic_uploads_deserialize() {
1199 let toml_str = r#"
1200[blossom]
1201optimistic_uploads = true
1202"#;
1203 let config: Config = toml::from_str(toml_str).unwrap();
1204 assert!(config.blossom.optimistic_uploads);
1205 assert!(config.blossom.require_random_untrusted_ingest);
1206 }
1207
1208 #[test]
1209 fn test_blossom_replication_deserialize() {
1210 let toml_str = r#"
1211[blossom]
1212replicate_servers = ["http://127.0.0.1:8081"]
1213replicate_queue_mb = 128
1214"#;
1215 let config: Config = toml::from_str(toml_str).unwrap();
1216 assert_eq!(config.blossom.replicate_servers, ["http://127.0.0.1:8081"]);
1217 assert_eq!(config.blossom.replicate_queue_mb, 128);
1218 }
1219
1220 #[test]
1221 fn test_server_public_plaintext_reads_deserialize() {
1222 let toml_str = r#"
1223[server]
1224public_plaintext_reads = false
1225"#;
1226 let config: Config = toml::from_str(toml_str).unwrap();
1227 assert!(!config.server.public_plaintext_reads);
1228 assert!(config.server.public_writes);
1229 }
1230
1231 #[test]
1232 fn test_nostr_config_deserialize_with_defaults() {
1233 let toml_str = r#"
1234[nostr]
1235relays = ["wss://relay.damus.io"]
1236"#;
1237 let config: Config = toml::from_str(toml_str).unwrap();
1238 assert!(config.nostr.enabled);
1239 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1240 assert!(config.storage.evict_orphans);
1241 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1242 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1243 assert_eq!(config.nostr.max_write_distance, 3);
1244 assert_eq!(config.nostr.db_max_size_gb, 10);
1245 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1246 assert!(!config.nostr.negentropy_only);
1247 assert_eq!(config.nostr.overmute_threshold, 1.0);
1248 assert_eq!(
1249 config.nostr.mirror_kinds,
1250 vec![0, 1, 3, 5, 6, 7, 16, 20, 1_111, 9_735, 10_000, 30_000, 30_023]
1251 );
1252 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1253 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1254 assert!(config.nostr.history_sync_on_reconnect);
1255 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1256 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1257 assert_eq!(config.nostr.archive_history_follow_distance, Some(2));
1258 assert_eq!(config.nostr.archive_history_max_relay_pages, 0);
1259 assert!(config.nostr.socialgraph_root.is_none());
1260 assert_eq!(
1261 config.nostr.bootstrap_follows,
1262 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1263 );
1264 }
1265
1266 #[test]
1267 fn test_nostr_config_deserialize_with_socialgraph() {
1268 let toml_str = r#"
1269[nostr]
1270relays = ["wss://relay.damus.io"]
1271socialgraph_root = "npub1test"
1272bootstrap_follows = []
1273social_graph_crawl_depth = 3
1274mirror_max_follow_distance = 2
1275max_write_distance = 5
1276negentropy_only = true
1277overmute_threshold = 2.5
1278mirror_kinds = [0, 10000]
1279history_sync_author_chunk_size = 250
1280history_sync_per_author_event_limit = 128
1281history_sync_on_reconnect = false
1282full_text_note_history_follow_distance = 1
1283full_text_note_history_max_relay_pages = 64
1284archive_history_follow_distance = 2
1285archive_history_max_relay_pages = 32
1286"#;
1287 let config: Config = toml::from_str(toml_str).unwrap();
1288 assert!(config.nostr.enabled);
1289 assert!(config.storage.evict_orphans);
1290 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1291 assert!(config.nostr.bootstrap_follows.is_empty());
1292 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1293 assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1294 assert_eq!(config.nostr.max_write_distance, 5);
1295 assert_eq!(config.nostr.db_max_size_gb, 10);
1296 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1297 assert!(config.nostr.negentropy_only);
1298 assert_eq!(config.nostr.overmute_threshold, 2.5);
1299 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1300 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1301 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1302 assert!(!config.nostr.history_sync_on_reconnect);
1303 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1304 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1305 assert_eq!(config.nostr.archive_history_follow_distance, Some(2));
1306 assert_eq!(config.nostr.archive_history_max_relay_pages, 32);
1307 }
1308
1309 #[test]
1310 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1311 let toml_str = r#"
1312[nostr]
1313relays = ["wss://relay.damus.io"]
1314crawl_depth = 4
1315"#;
1316 let config: Config = toml::from_str(toml_str).unwrap();
1317 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1318 }
1319
1320 #[test]
1321 fn test_storage_config_disables_orphan_eviction_when_requested() {
1322 let toml_str = r#"
1323[storage]
1324evict_orphans = false
1325"#;
1326 let config: Config = toml::from_str(toml_str).unwrap();
1327 assert!(!config.storage.evict_orphans);
1328 }
1329
1330 #[test]
1331 fn test_cashu_config_deserialize_with_accepted_mints() {
1332 let toml_str = r#"
1333[cashu]
1334accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1335default_mint = "https://mint1.example"
1336quote_payment_offer_sat = 5
1337quote_ttl_ms = 2500
1338settlement_timeout_ms = 7000
1339mint_failure_block_threshold = 3
1340peer_suggested_mint_base_cap_sat = 4
1341peer_suggested_mint_success_step_sat = 2
1342peer_suggested_mint_receipt_step_sat = 3
1343peer_suggested_mint_max_cap_sat = 34
1344payment_default_block_threshold = 2
1345chunk_target_bytes = 65536
1346"#;
1347 let config: Config = toml::from_str(toml_str).unwrap();
1348 assert_eq!(
1349 config.cashu.accepted_mints,
1350 vec![
1351 "https://mint1.example".to_string(),
1352 "http://127.0.0.1:3338".to_string()
1353 ]
1354 );
1355 assert_eq!(
1356 config.cashu.default_mint,
1357 Some("https://mint1.example".to_string())
1358 );
1359 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1360 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1361 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1362 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1363 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1364 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1365 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1366 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1367 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1368 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1369 }
1370
1371 #[test]
1372 fn test_auth_cookie_generation() -> Result<()> {
1373 let _lock = test_env_lock().blocking_lock();
1374 let temp_dir = TempDir::new()?;
1375 let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1376
1377 let (username, password) = generate_auth_cookie()?;
1378
1379 assert!(username.starts_with("htree_"));
1380 assert_eq!(password.len(), 32);
1381
1382 let cookie_path = get_auth_cookie_path();
1384 assert!(cookie_path.exists());
1385
1386 let (u2, p2) = read_auth_cookie()?;
1388 assert_eq!(username, u2);
1389 assert_eq!(password, p2);
1390
1391 Ok(())
1392 }
1393
1394 #[test]
1395 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1396 let config = BlossomConfig {
1397 servers: vec!["https://legacy.server".to_string()],
1398 ..BlossomConfig::default()
1399 };
1400
1401 let read = config.all_read_servers();
1402 assert!(read.contains(&"https://legacy.server".to_string()));
1403 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1404 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1405 assert!(read.contains(&"https://upload.iris.to".to_string()));
1406
1407 let write = config.all_write_servers();
1408 assert!(write.contains(&"https://legacy.server".to_string()));
1409 assert!(write.contains(&"https://upload.iris.to".to_string()));
1410 }
1411
1412 #[test]
1413 fn daemon_blossom_upstreams_exclude_its_own_loopback_http_endpoint() {
1414 let config = BlossomConfig {
1415 servers: Vec::new(),
1416 read_servers: vec![
1417 "http://127.0.0.1:19092".to_string(),
1418 "http://localhost:19092/".to_string(),
1419 "http://127.0.0.1:19093".to_string(),
1420 "https://127.0.0.1:19092".to_string(),
1421 "https://read.example".to_string(),
1422 ],
1423 write_servers: Vec::new(),
1424 ..BlossomConfig::default()
1425 };
1426
1427 let upstreams = config.upstream_read_servers("127.0.0.1:19092");
1428
1429 assert!(!upstreams
1430 .iter()
1431 .any(|server| server == "http://127.0.0.1:19092"));
1432 assert!(!upstreams
1433 .iter()
1434 .any(|server| server == "http://localhost:19092/"));
1435 assert!(upstreams
1436 .iter()
1437 .any(|server| server == "http://127.0.0.1:19093"));
1438 assert!(upstreams
1439 .iter()
1440 .any(|server| server == "https://127.0.0.1:19092"));
1441 assert!(upstreams
1442 .iter()
1443 .any(|server| server == "https://read.example"));
1444 }
1445
1446 #[test]
1447 fn wildcard_daemon_bind_excludes_loopback_self_but_keeps_remote_upstreams() {
1448 let config = BlossomConfig {
1449 servers: Vec::new(),
1450 read_servers: vec![
1451 "http://localhost:8080".to_string(),
1452 "http://[::1]:8080".to_string(),
1453 "http://192.0.2.10:8080".to_string(),
1454 ],
1455 write_servers: Vec::new(),
1456 ..BlossomConfig::default()
1457 };
1458
1459 let upstreams = config.upstream_read_servers("0.0.0.0:8080");
1460
1461 assert!(!upstreams
1462 .iter()
1463 .any(|server| server == "http://localhost:8080"));
1464 assert!(!upstreams.iter().any(|server| server == "http://[::1]:8080"));
1465 assert!(upstreams
1466 .iter()
1467 .any(|server| server == "http://192.0.2.10:8080"));
1468 }
1469
1470 #[test]
1471 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1472 let config = BlossomConfig {
1473 enabled: true,
1474 servers: Vec::new(),
1475 read_servers: Vec::new(),
1476 write_servers: Vec::new(),
1477 max_upload_mb: default_max_upload_mb(),
1478 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1479 optimistic_uploads: default_optimistic_uploads(),
1480 replicate_servers: Vec::new(),
1481 replicate_queue_mb: default_replicate_queue_mb(),
1482 };
1483
1484 let read = config.all_read_servers();
1485 let mut expected = default_read_servers();
1486 expected.extend(default_write_servers());
1487 expected.sort();
1488 expected.dedup();
1489 assert_eq!(read, expected);
1490
1491 let write = config.all_write_servers();
1492 assert_eq!(write, default_write_servers());
1493 }
1494
1495 #[test]
1496 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1497 let nostr = NostrConfig {
1498 enabled: false,
1499 relays: vec!["wss://relay.example".to_string()],
1500 ..NostrConfig::default()
1501 };
1502 assert!(nostr.active_relays().is_empty());
1503
1504 let blossom = BlossomConfig {
1505 enabled: false,
1506 servers: vec!["https://legacy.server".to_string()],
1507 read_servers: vec!["https://read.example".to_string()],
1508 write_servers: vec!["https://write.example".to_string()],
1509 max_upload_mb: default_max_upload_mb(),
1510 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1511 optimistic_uploads: default_optimistic_uploads(),
1512 replicate_servers: Vec::new(),
1513 replicate_queue_mb: default_replicate_queue_mb(),
1514 };
1515 assert!(blossom.all_read_servers().is_empty());
1516 assert!(blossom.all_write_servers().is_empty());
1517 }
1518
1519 #[test]
1520 fn fips_local_only_event_transport_never_exposes_direct_relays() {
1521 let nostr = NostrConfig {
1522 relays: vec!["wss://must-not-open.example".to_string()],
1523 event_transport: NostrEventTransport::FipsLocalOnly,
1524 ..NostrConfig::default()
1525 };
1526
1527 assert!(nostr.active_relays().is_empty());
1528 }
1529
1530 #[test]
1531 fn nostr_decentralized_pubsub_requires_config_and_feature() {
1532 let default_nostr = NostrConfig::default();
1533 assert!(!default_nostr.decentralized_pubsub);
1534 assert!(!default_nostr.decentralized_pubsub_enabled());
1535
1536 let enabled: NostrConfig = toml::from_str("decentralized_pubsub = true")
1537 .expect("parse decentralized pubsub nostr config");
1538 assert!(enabled.decentralized_pubsub);
1539 assert_eq!(
1540 enabled.decentralized_pubsub_max_event_bytes,
1541 nostr_pubsub_fips::FIPS_NOSTR_PUBSUB_MAX_DATAGRAM_BYTES
1542 );
1543 assert_eq!(
1544 enabled.decentralized_pubsub_enabled(),
1545 cfg!(feature = "experimental-decentralized-pubsub")
1546 );
1547
1548 let disabled: NostrConfig = toml::from_str(
1549 r#"
1550enabled = false
1551decentralized_pubsub = true
1552"#,
1553 )
1554 .expect("parse disabled decentralized pubsub nostr config");
1555 assert!(!disabled.decentralized_pubsub_enabled());
1556
1557 let alias: NostrConfig =
1558 toml::from_str("relayless_pubsub = true").expect("parse compatibility pubsub alias");
1559 assert!(alias.decentralized_pubsub);
1560
1561 let tuned: NostrConfig = toml::from_str(
1562 r#"
1563decentralized_pubsub = true
1564decentralized_pubsub_max_event_bytes = 4096
1565"#,
1566 )
1567 .expect("parse tuned decentralized pubsub config");
1568 assert_eq!(tuned.decentralized_pubsub_max_event_bytes, 4096);
1569 }
1570
1571 #[test]
1572 fn server_defaults_enable_fips_udp_and_feature_gated_webrtc() {
1573 let server = ServerConfig::default();
1574
1575 assert!(server.enable_fips);
1576 assert!(server.enable_fips_udp);
1577 assert!(server.fips_udp_bind_addr.is_none());
1578 assert!(!server.fips_udp_public);
1579 assert!(server.fips_udp_external_addr.is_none());
1580 assert_eq!(server.enable_fips_webrtc, cfg!(feature = "fips-webrtc"));
1581 assert!(server.fips_ethernet_interfaces.is_empty());
1582 assert!(server.fetch_from_fips_peers);
1583 assert!(server.fips_relays.is_none());
1584 assert!(server.fips_peers.is_empty());
1585 assert_eq!(server.fips_discovery_scope, "fips-overlay-v1");
1586 assert_eq!(server.fips_open_discovery_max_pending, 0);
1587 assert!(server.fips_local_rendezvous_addr.is_none());
1588 assert!(server.enable_fips_lan_discovery);
1589 assert_eq!(server.fips_request_timeout_ms, 5_500);
1590 }
1591
1592 #[test]
1593 fn server_config_reads_fips_overrides() {
1594 let config: Config = toml::from_str(
1595 r#"
1596[server]
1597enable_fips = true
1598fips_discovery_scope = "test-hashtree"
1599fips_open_discovery_max_pending = 32
1600fips_local_rendezvous_addr = "127.0.0.1:32111"
1601enable_fips_lan_discovery = false
1602fips_relays = ["wss://fips.example"]
1603fips_peers = [
1604 { npub = "npub1origin", udp_addresses = ["udp:192.0.2.10:2121"] },
1605 { npub = "npub1cache" },
1606]
1607enable_fips_udp = false
1608fips_udp_bind_addr = "0.0.0.0:2121"
1609fips_udp_public = true
1610fips_udp_external_addr = "198.19.77.10:2121"
1611enable_fips_webrtc = true
1612fips_ethernet_interfaces = ["eth0"]
1613fetch_from_fips_peers = false
1614fips_request_timeout_ms = 42
1615"#,
1616 )
1617 .unwrap();
1618
1619 assert!(config.server.enable_fips);
1620 assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1621 assert_eq!(config.server.fips_open_discovery_max_pending, 32);
1622 assert_eq!(
1623 config.server.fips_local_rendezvous_addr.as_deref(),
1624 Some("127.0.0.1:32111")
1625 );
1626 assert!(!config.server.enable_fips_lan_discovery);
1627 assert_eq!(
1628 config.server.fips_relays,
1629 Some(vec!["wss://fips.example".to_string()])
1630 );
1631 assert_eq!(
1632 config.server.fips_peers,
1633 [
1634 ConfiguredFipsPeer {
1635 npub: "npub1origin".to_string(),
1636 udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
1637 },
1638 ConfiguredFipsPeer {
1639 npub: "npub1cache".to_string(),
1640 udp_addresses: Vec::new(),
1641 },
1642 ]
1643 );
1644 assert!(!config.server.enable_fips_udp);
1645 assert_eq!(
1646 config.server.fips_udp_bind_addr.as_deref(),
1647 Some("0.0.0.0:2121")
1648 );
1649 assert!(config.server.fips_udp_public);
1650 assert_eq!(
1651 config.server.fips_udp_external_addr.as_deref(),
1652 Some("198.19.77.10:2121")
1653 );
1654 assert!(config.server.enable_fips_webrtc);
1655 assert_eq!(config.server.fips_ethernet_interfaces, ["eth0"]);
1656 assert!(!config.server.fetch_from_fips_peers);
1657 assert_eq!(config.server.fips_request_timeout_ms, 42);
1658 }
1659
1660 #[test]
1661 fn server_config_accepts_legacy_http_fips_fetch_name() {
1662 let config: Config = toml::from_str(
1663 r#"
1664[server]
1665http_fips_fetch = false
1666"#,
1667 )
1668 .unwrap();
1669
1670 assert!(!config.server.fetch_from_fips_peers);
1671 }
1672
1673 #[test]
1674 fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1675 let active_nostr = vec!["wss://nostr.example".to_string()];
1676 let mut server = ServerConfig::default();
1677
1678 assert_eq!(
1679 server.resolved_fips_relays(&active_nostr),
1680 [
1681 "wss://nostr.example",
1682 "wss://temp.iris.to",
1683 "wss://relay.primal.net"
1684 ]
1685 );
1686
1687 server.fips_relays = Some(vec!["wss://fips.example".to_string()]);
1688 assert_eq!(
1689 server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1690 ["wss://fips.example"]
1691 );
1692 }
1693
1694 #[test]
1695 fn explicit_fips_relay_resolution_is_exact_and_normalized() {
1696 let server = ServerConfig {
1697 fips_relays: Some(vec![
1698 "wss://temp.iris.to/".to_string(),
1699 " wss://relay.primal.net ".to_string(),
1700 "wss://temp.iris.to".to_string(),
1701 "wss://extra.example".to_string(),
1702 ]),
1703 ..ServerConfig::default()
1704 };
1705
1706 assert_eq!(
1707 server.resolved_fips_relays(&[]),
1708 [
1709 "wss://temp.iris.to",
1710 "wss://relay.primal.net",
1711 "wss://extra.example"
1712 ]
1713 );
1714 }
1715
1716 #[test]
1717 fn explicit_empty_fips_relays_disable_all_relay_discovery() {
1718 let config: Config = toml::from_str(
1719 r#"
1720[server]
1721enable_fips = true
1722enable_fips_udp = false
1723enable_fips_webrtc = false
1724fips_ethernet_interfaces = ["eth0"]
1725fips_relays = []
1726
1727[nostr]
1728relays = ["wss://must-not-open.example"]
1729event_transport = "fips-local-only"
1730"#,
1731 )
1732 .unwrap();
1733
1734 assert_eq!(config.server.fips_relays, Some(Vec::new()));
1735 assert_eq!(
1736 config.nostr.event_transport,
1737 NostrEventTransport::FipsLocalOnly
1738 );
1739 assert!(config.nostr.active_relays().is_empty());
1740 assert!(config
1741 .server
1742 .resolved_fips_relays(&["wss://must-not-open.example".to_string()])
1743 .is_empty());
1744 }
1745}