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