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)]
108 pub fips_relays: Vec<String>,
109 #[serde(
112 default,
113 alias = "preconfigured_fips_peers",
114 alias = "fips_static_peers"
115 )]
116 pub fips_peers: Vec<ConfiguredFipsPeer>,
117 #[serde(default = "default_enable_fips_udp")]
119 pub enable_fips_udp: bool,
120 #[serde(default)]
122 pub fips_udp_bind_addr: Option<String>,
123 #[serde(default)]
125 pub fips_udp_public: bool,
126 #[serde(default)]
128 pub fips_udp_external_addr: Option<String>,
129 #[serde(default = "default_enable_fips_webrtc")]
131 pub enable_fips_webrtc: bool,
132 #[serde(default = "default_fetch_from_fips_peers", alias = "http_fips_fetch")]
134 pub fetch_from_fips_peers: bool,
135 #[serde(default = "default_fips_request_timeout_ms")]
137 pub fips_request_timeout_ms: u64,
138 #[serde(default = "default_http_webrtc_fetch")]
140 pub http_webrtc_fetch: bool,
141 #[serde(default, alias = "peer_direct_urls", alias = "peer_advertise_urls")]
144 pub peer_signal_urls: Vec<String>,
145 #[serde(default = "default_enable_multicast")]
147 pub enable_multicast: bool,
148 #[serde(default = "default_multicast_group")]
150 pub multicast_group: String,
151 #[serde(default = "default_multicast_port")]
153 pub multicast_port: u16,
154 #[serde(default = "default_max_multicast_peers")]
157 pub max_multicast_peers: usize,
158 #[serde(default = "default_enable_wifi_aware")]
160 pub enable_wifi_aware: bool,
161 #[serde(default = "default_max_wifi_aware_peers")]
164 pub max_wifi_aware_peers: usize,
165 #[serde(default = "default_enable_bluetooth")]
167 pub enable_bluetooth: bool,
168 #[serde(default = "default_max_bluetooth_peers")]
171 pub max_bluetooth_peers: usize,
172 #[serde(default = "default_public_writes")]
175 pub public_writes: bool,
176 #[serde(default = "default_public_plaintext_reads")]
179 pub public_plaintext_reads: bool,
180 #[serde(default = "default_socialgraph_snapshot_public")]
182 pub socialgraph_snapshot_public: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ConfiguredFipsPeer {
187 pub npub: String,
188 #[serde(default)]
189 pub udp_addresses: Vec<String>,
190}
191
192fn default_public_writes() -> bool {
193 true
194}
195
196fn default_public_plaintext_reads() -> bool {
197 true
198}
199
200fn default_socialgraph_snapshot_public() -> bool {
201 false
202}
203
204impl ServerConfig {
205 pub fn resolved_fips_relays(&self, active_nostr_relays: &[String]) -> Vec<String> {
206 let configured = if self.fips_relays.is_empty() {
207 active_nostr_relays
208 } else {
209 &self.fips_relays
210 };
211 merge_fips_signal_relays(configured)
212 }
213}
214
215const DEFAULT_FIPS_SIGNAL_RELAYS: [&str; 2] = ["wss://temp.iris.to", "wss://relay.primal.net"];
216
217fn merge_fips_signal_relays(configured: &[String]) -> Vec<String> {
218 let mut relays = Vec::new();
219 for relay in configured
220 .iter()
221 .map(String::as_str)
222 .chain(DEFAULT_FIPS_SIGNAL_RELAYS)
223 {
224 let normalized = relay.trim().trim_end_matches('/').to_string();
225 if normalized.is_empty() || relays.contains(&normalized) {
226 continue;
227 }
228 relays.push(normalized);
229 }
230 relays
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct StorageConfig {
235 #[serde(default = "default_data_dir")]
236 pub data_dir: String,
237 #[serde(default = "default_max_size_gb")]
238 pub max_size_gb: u64,
239 #[serde(default = "default_storage_evict_orphans")]
240 pub evict_orphans: bool,
241 #[serde(default)]
243 pub s3: Option<S3Config>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct S3Config {
249 pub endpoint: String,
251 pub bucket: String,
253 #[serde(default)]
255 pub prefix: Option<String>,
256 #[serde(default = "default_s3_region")]
258 pub region: String,
259 #[serde(default)]
261 pub access_key: Option<String>,
262 #[serde(default)]
264 pub secret_key: Option<String>,
265 #[serde(default)]
267 pub public_url: Option<String>,
268}
269
270fn default_s3_region() -> String {
271 "auto".to_string()
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct NostrConfig {
276 #[serde(default = "default_nostr_enabled")]
277 pub enabled: bool,
278 #[serde(default = "default_relays")]
279 pub relays: Vec<String>,
280 #[serde(default)]
282 pub allowed_npubs: Vec<String>,
283 #[serde(default)]
285 pub socialgraph_root: Option<String>,
286 #[serde(default = "default_nostr_bootstrap_follows")]
289 pub bootstrap_follows: Vec<String>,
290 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
292 pub social_graph_crawl_depth: u32,
293 #[serde(default)]
296 pub mirror_max_follow_distance: Option<u32>,
297 #[serde(default = "default_max_write_distance")]
299 pub max_write_distance: u32,
300 #[serde(default = "default_nostr_db_max_size_gb")]
302 pub db_max_size_gb: u64,
303 #[serde(default = "default_nostr_spambox_max_size_gb")]
306 pub spambox_max_size_gb: u64,
307 #[serde(default)]
309 pub negentropy_only: bool,
310 #[serde(default = "default_nostr_overmute_threshold")]
312 pub overmute_threshold: f64,
313 #[serde(default = "default_nostr_mirror_kinds")]
315 pub mirror_kinds: Vec<u16>,
316 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
318 pub history_sync_author_chunk_size: usize,
319 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
321 pub history_sync_per_author_event_limit: usize,
322 #[serde(default = "default_nostr_history_sync_on_reconnect")]
324 pub history_sync_on_reconnect: bool,
325 #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
328 pub full_text_note_history_follow_distance: Option<u32>,
329 #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
332 pub full_text_note_history_max_relay_pages: usize,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct BlossomConfig {
337 #[serde(default = "default_blossom_enabled")]
338 pub enabled: bool,
339 #[serde(default)]
341 pub servers: Vec<String>,
342 #[serde(default = "default_read_servers")]
344 pub read_servers: Vec<String>,
345 #[serde(default = "default_write_servers")]
347 pub write_servers: Vec<String>,
348 #[serde(default = "default_max_upload_mb")]
350 pub max_upload_mb: u64,
351 #[serde(default = "default_require_random_untrusted_ingest")]
353 pub require_random_untrusted_ingest: bool,
354 #[serde(default = "default_optimistic_uploads")]
357 pub optimistic_uploads: bool,
358 #[serde(
362 default,
363 alias = "write_behind_servers",
364 alias = "mirror_write_servers"
365 )]
366 pub replicate_servers: Vec<String>,
367 #[serde(default = "default_replicate_queue_mb")]
369 pub replicate_queue_mb: u64,
370}
371
372impl BlossomConfig {
373 pub fn all_read_servers(&self) -> Vec<String> {
374 if !self.enabled {
375 return Vec::new();
376 }
377 let mut servers = self.servers.clone();
378 servers.extend(self.read_servers.clone());
379 servers.extend(self.write_servers.clone());
380 if servers.is_empty() {
381 servers = default_read_servers();
382 servers.extend(default_write_servers());
383 }
384 servers.sort();
385 servers.dedup();
386 servers
387 }
388
389 pub fn all_write_servers(&self) -> Vec<String> {
390 if !self.enabled {
391 return Vec::new();
392 }
393 let mut servers = self.servers.clone();
394 servers.extend(self.write_servers.clone());
395 if servers.is_empty() {
396 servers = default_write_servers();
397 }
398 servers.sort();
399 servers.dedup();
400 servers
401 }
402}
403
404impl NostrConfig {
405 pub fn active_relays(&self) -> Vec<String> {
406 if self.enabled {
407 self.relays.clone()
408 } else {
409 Vec::new()
410 }
411 }
412}
413
414fn default_read_servers() -> Vec<String> {
416 let mut servers = vec![
417 "https://blossom.primal.net".to_string(),
418 "https://cdn.iris.to".to_string(),
419 ];
420 servers.sort();
421 servers
422}
423
424fn default_write_servers() -> Vec<String> {
425 vec!["https://upload.iris.to".to_string()]
426}
427
428fn default_max_upload_mb() -> u64 {
429 5
430}
431
432fn default_require_random_untrusted_ingest() -> bool {
433 true
434}
435
436fn default_optimistic_uploads() -> bool {
437 false
438}
439
440fn default_replicate_queue_mb() -> u64 {
441 512
442}
443
444fn default_nostr_enabled() -> bool {
445 true
446}
447
448fn default_blossom_enabled() -> bool {
449 true
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct SyncConfig {
454 #[serde(default = "default_sync_enabled")]
456 pub enabled: bool,
457 #[serde(default = "default_sync_own")]
459 pub sync_own: bool,
460 #[serde(default = "default_sync_followed")]
462 pub sync_followed: bool,
463 #[serde(default = "default_max_concurrent")]
465 pub max_concurrent: usize,
466 #[serde(default = "default_webrtc_timeout_ms")]
468 pub webrtc_timeout_ms: u64,
469 #[serde(default = "default_blossom_timeout_ms")]
471 pub blossom_timeout_ms: u64,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct CashuConfig {
476 #[serde(default)]
478 pub accepted_mints: Vec<String>,
479 #[serde(default)]
481 pub default_mint: Option<String>,
482 #[serde(default = "default_cashu_quote_payment_offer_sat")]
484 pub quote_payment_offer_sat: u64,
485 #[serde(default = "default_cashu_quote_ttl_ms")]
487 pub quote_ttl_ms: u32,
488 #[serde(default = "default_cashu_settlement_timeout_ms")]
490 pub settlement_timeout_ms: u64,
491 #[serde(default = "default_cashu_mint_failure_block_threshold")]
493 pub mint_failure_block_threshold: u64,
494 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
496 pub peer_suggested_mint_base_cap_sat: u64,
497 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
499 pub peer_suggested_mint_success_step_sat: u64,
500 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
502 pub peer_suggested_mint_receipt_step_sat: u64,
503 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
505 pub peer_suggested_mint_max_cap_sat: u64,
506 #[serde(default)]
508 pub payment_default_block_threshold: u64,
509 #[serde(default = "default_cashu_chunk_target_bytes")]
511 pub chunk_target_bytes: usize,
512}
513
514impl Default for CashuConfig {
515 fn default() -> Self {
516 Self {
517 accepted_mints: Vec::new(),
518 default_mint: None,
519 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
520 quote_ttl_ms: default_cashu_quote_ttl_ms(),
521 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
522 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
523 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
524 peer_suggested_mint_success_step_sat:
525 default_cashu_peer_suggested_mint_success_step_sat(),
526 peer_suggested_mint_receipt_step_sat:
527 default_cashu_peer_suggested_mint_receipt_step_sat(),
528 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
529 payment_default_block_threshold: 0,
530 chunk_target_bytes: default_cashu_chunk_target_bytes(),
531 }
532 }
533}
534
535fn default_cashu_quote_payment_offer_sat() -> u64 {
536 3
537}
538
539fn default_cashu_quote_ttl_ms() -> u32 {
540 1_500
541}
542
543fn default_cashu_settlement_timeout_ms() -> u64 {
544 5_000
545}
546
547fn default_cashu_mint_failure_block_threshold() -> u64 {
548 2
549}
550
551fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
552 3
553}
554
555fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
556 1
557}
558
559fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
560 2
561}
562
563fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
564 21
565}
566
567fn default_cashu_chunk_target_bytes() -> usize {
568 32 * 1024
569}
570
571fn default_sync_enabled() -> bool {
572 true
573}
574
575fn default_sync_own() -> bool {
576 true
577}
578
579fn default_sync_followed() -> bool {
580 true
581}
582
583fn default_max_concurrent() -> usize {
584 3
585}
586
587fn default_webrtc_timeout_ms() -> u64 {
588 2000
589}
590
591fn default_blossom_timeout_ms() -> u64 {
592 10000
593}
594
595fn default_social_graph_crawl_depth() -> u32 {
596 2
597}
598
599fn default_nostr_bootstrap_follows() -> Vec<String> {
600 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
601}
602
603fn default_max_write_distance() -> u32 {
604 3
605}
606
607fn default_nostr_db_max_size_gb() -> u64 {
608 10
609}
610
611fn default_nostr_spambox_max_size_gb() -> u64 {
612 1
613}
614
615fn default_nostr_history_sync_on_reconnect() -> bool {
616 true
617}
618
619fn default_nostr_overmute_threshold() -> f64 {
620 1.0
621}
622
623fn default_nostr_mirror_kinds() -> Vec<u16> {
624 vec![0, 1, 3, 6, 7, 9_735, 30_023]
625}
626
627fn default_nostr_history_sync_author_chunk_size() -> usize {
628 5_000
629}
630
631fn default_nostr_history_sync_per_author_event_limit() -> usize {
632 256
633}
634
635fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
636 Some(2)
637}
638
639fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
640 0
641}
642
643fn default_relays() -> Vec<String> {
644 vec![
645 "wss://nos.lol".to_string(),
646 "wss://relay.snort.social".to_string(),
647 "wss://temp.iris.to".to_string(),
648 ]
649}
650
651fn default_bind_address() -> String {
652 "127.0.0.1:8080".to_string()
653}
654
655fn default_enable_auth() -> bool {
656 true
657}
658
659fn default_stun_port() -> u16 {
660 3478 }
662
663fn default_enable_webrtc() -> bool {
664 true
665}
666
667fn default_enable_fips() -> bool {
668 true
669}
670
671fn default_fips_discovery_scope() -> String {
672 "hashtree-v1".to_string()
673}
674
675fn default_enable_fips_udp() -> bool {
676 true
677}
678
679fn default_enable_fips_webrtc() -> bool {
680 true
681}
682
683fn default_fetch_from_fips_peers() -> bool {
684 true
685}
686
687fn default_fips_request_timeout_ms() -> u64 {
688 5_500
689}
690
691fn default_http_webrtc_fetch() -> bool {
692 true
693}
694
695fn default_enable_multicast() -> bool {
696 true
697}
698
699fn default_multicast_group() -> String {
700 "239.255.42.98".to_string()
701}
702
703fn default_multicast_port() -> u16 {
704 48555
705}
706
707fn default_max_multicast_peers() -> usize {
708 12
709}
710
711fn default_enable_wifi_aware() -> bool {
712 false
713}
714
715fn default_max_wifi_aware_peers() -> usize {
716 0
717}
718
719fn default_enable_bluetooth() -> bool {
720 false
721}
722
723fn default_max_bluetooth_peers() -> usize {
724 0
725}
726
727fn default_data_dir() -> String {
728 hashtree_config::get_hashtree_dir()
729 .join("data")
730 .to_string_lossy()
731 .to_string()
732}
733
734fn default_max_size_gb() -> u64 {
735 10
736}
737
738fn default_storage_evict_orphans() -> bool {
739 true
740}
741
742impl Default for ServerConfig {
743 fn default() -> Self {
744 Self {
745 mode: ServerMode::default(),
746 bind_address: default_bind_address(),
747 enable_auth: default_enable_auth(),
748 stun_port: default_stun_port(),
749 enable_webrtc: default_enable_webrtc(),
750 enable_fips: default_enable_fips(),
751 fips_discovery_scope: default_fips_discovery_scope(),
752 fips_relays: Vec::new(),
753 fips_peers: Vec::new(),
754 enable_fips_udp: default_enable_fips_udp(),
755 fips_udp_bind_addr: None,
756 fips_udp_public: false,
757 fips_udp_external_addr: None,
758 enable_fips_webrtc: default_enable_fips_webrtc(),
759 fetch_from_fips_peers: default_fetch_from_fips_peers(),
760 fips_request_timeout_ms: default_fips_request_timeout_ms(),
761 http_webrtc_fetch: default_http_webrtc_fetch(),
762 peer_signal_urls: Vec::new(),
763 enable_multicast: default_enable_multicast(),
764 multicast_group: default_multicast_group(),
765 multicast_port: default_multicast_port(),
766 max_multicast_peers: default_max_multicast_peers(),
767 enable_wifi_aware: default_enable_wifi_aware(),
768 max_wifi_aware_peers: default_max_wifi_aware_peers(),
769 enable_bluetooth: default_enable_bluetooth(),
770 max_bluetooth_peers: default_max_bluetooth_peers(),
771 public_writes: default_public_writes(),
772 public_plaintext_reads: default_public_plaintext_reads(),
773 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
774 }
775 }
776}
777
778impl Default for StorageConfig {
779 fn default() -> Self {
780 Self {
781 data_dir: default_data_dir(),
782 max_size_gb: default_max_size_gb(),
783 evict_orphans: default_storage_evict_orphans(),
784 s3: None,
785 }
786 }
787}
788
789impl Default for NostrConfig {
790 fn default() -> Self {
791 Self {
792 enabled: default_nostr_enabled(),
793 relays: default_relays(),
794 allowed_npubs: Vec::new(),
795 socialgraph_root: None,
796 bootstrap_follows: default_nostr_bootstrap_follows(),
797 social_graph_crawl_depth: default_social_graph_crawl_depth(),
798 mirror_max_follow_distance: None,
799 max_write_distance: default_max_write_distance(),
800 db_max_size_gb: default_nostr_db_max_size_gb(),
801 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
802 negentropy_only: false,
803 overmute_threshold: default_nostr_overmute_threshold(),
804 mirror_kinds: default_nostr_mirror_kinds(),
805 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
806 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
807 ),
808 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
809 full_text_note_history_follow_distance:
810 default_nostr_full_text_note_history_follow_distance(),
811 full_text_note_history_max_relay_pages:
812 default_nostr_full_text_note_history_max_relay_pages(),
813 }
814 }
815}
816
817impl Default for BlossomConfig {
818 fn default() -> Self {
819 Self {
820 enabled: default_blossom_enabled(),
821 servers: Vec::new(),
822 read_servers: default_read_servers(),
823 write_servers: default_write_servers(),
824 max_upload_mb: default_max_upload_mb(),
825 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
826 optimistic_uploads: default_optimistic_uploads(),
827 replicate_servers: Vec::new(),
828 replicate_queue_mb: default_replicate_queue_mb(),
829 }
830 }
831}
832
833impl Default for SyncConfig {
834 fn default() -> Self {
835 Self {
836 enabled: default_sync_enabled(),
837 sync_own: default_sync_own(),
838 sync_followed: default_sync_followed(),
839 max_concurrent: default_max_concurrent(),
840 webrtc_timeout_ms: default_webrtc_timeout_ms(),
841 blossom_timeout_ms: default_blossom_timeout_ms(),
842 }
843 }
844}
845
846impl Config {
847 pub fn load() -> Result<Self> {
849 let config_path = get_config_path();
850
851 if config_path.exists() {
852 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
853 toml::from_str(&content).context("Failed to parse config file")
854 } else {
855 let config = Config::default();
856 config.save()?;
857 Ok(config)
858 }
859 }
860
861 pub fn save(&self) -> Result<()> {
863 let config_path = get_config_path();
864
865 if let Some(parent) = config_path.parent() {
867 fs::create_dir_all(parent)?;
868 }
869
870 let content = toml::to_string_pretty(self)?;
871 fs::write(&config_path, content)?;
872
873 Ok(())
874 }
875}
876
877pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
879
880fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
881 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
882 let entries = hashtree_config::parse_keys_file(&content);
883 let nsec_str = entries
884 .into_iter()
885 .next()
886 .map(|e| e.secret)
887 .context("Keys file is empty")?;
888 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
889 Ok(Keys::new(secret_key))
890}
891
892fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
893 if let (Some(data_dir), Some(config)) = (data_dir, config) {
894 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
895 }
896}
897
898fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
899 if let Some(parent) = keys_path.parent() {
900 fs::create_dir_all(parent)?;
901 }
902
903 let nsec = keys
904 .secret_key()
905 .to_bech32()
906 .context("Failed to encode nsec")?;
907 fs::write(keys_path, &nsec)?;
908
909 #[cfg(unix)]
910 {
911 use std::os::unix::fs::PermissionsExt;
912 let perms = fs::Permissions::from_mode(0o600);
913 fs::set_permissions(keys_path, perms)?;
914 }
915
916 Ok(())
917}
918
919pub fn ensure_auth_cookie() -> Result<(String, String)> {
921 let cookie_path = get_auth_cookie_path();
922
923 if cookie_path.exists() {
924 read_auth_cookie()
925 } else {
926 generate_auth_cookie()
927 }
928}
929
930pub fn read_auth_cookie() -> Result<(String, String)> {
932 let cookie_path = get_auth_cookie_path();
933 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
934
935 let parts: Vec<&str> = content.trim().split(':').collect();
936 if parts.len() != 2 {
937 anyhow::bail!("Invalid auth cookie format");
938 }
939
940 Ok((parts[0].to_string(), parts[1].to_string()))
941}
942
943pub fn ensure_keys() -> Result<(Keys, bool)> {
946 let config_dir = get_hashtree_dir();
947 let config = Config::load().ok();
948 let data_dir = config
949 .as_ref()
950 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
951 ensure_keys_in(&config_dir, data_dir, config.as_ref())
952}
953
954pub fn ensure_keys_in(
957 config_dir: &Path,
958 data_dir: Option<&Path>,
959 config: Option<&Config>,
960) -> Result<(Keys, bool)> {
961 let keys_path = config_dir.join("keys");
962
963 if keys_path.exists() {
964 Ok((read_keys_from_path(&keys_path)?, false))
965 } else {
966 let keys = generate_keys_in(config_dir, data_dir, config)?;
967 Ok((keys, true))
968 }
969}
970
971pub fn read_keys() -> Result<Keys> {
973 read_keys_in(&get_hashtree_dir())
974}
975
976pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
978 read_keys_from_path(&config_dir.join("keys"))
979}
980
981pub fn ensure_keys_string() -> Result<(String, bool)> {
984 let config_dir = get_hashtree_dir();
985 let config = Config::load().ok();
986 let data_dir = config
987 .as_ref()
988 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
989 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
990}
991
992pub fn ensure_keys_string_in(
995 config_dir: &Path,
996 data_dir: Option<&Path>,
997 config: Option<&Config>,
998) -> Result<(String, bool)> {
999 let keys_path = config_dir.join("keys");
1000
1001 if keys_path.exists() {
1002 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
1003 let entries = hashtree_config::parse_keys_file(&content);
1004 let nsec_str = entries
1005 .into_iter()
1006 .next()
1007 .map(|e| e.secret)
1008 .context("Keys file is empty")?;
1009 Ok((nsec_str, false))
1010 } else {
1011 let keys = generate_keys_in(config_dir, data_dir, config)?;
1012 let nsec = keys
1013 .secret_key()
1014 .to_bech32()
1015 .context("Failed to encode nsec")?;
1016 Ok((nsec, true))
1017 }
1018}
1019
1020pub fn generate_keys() -> Result<Keys> {
1022 let config_dir = get_hashtree_dir();
1023 let config = Config::load().ok();
1024 let data_dir = config
1025 .as_ref()
1026 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
1027 generate_keys_in(&config_dir, data_dir, config.as_ref())
1028}
1029
1030pub fn generate_keys_in(
1033 config_dir: &Path,
1034 data_dir: Option<&Path>,
1035 config: Option<&Config>,
1036) -> Result<Keys> {
1037 let keys = Keys::generate();
1038 write_keys_to_path(&config_dir.join("keys"), &keys)?;
1039 seed_identity_defaults_if_needed(data_dir, config);
1040 Ok(keys)
1041}
1042
1043pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
1045 keys.public_key().to_bytes()
1046}
1047
1048pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
1050 use nostr::PublicKey;
1051 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
1052 Ok(pk.to_bytes())
1053}
1054
1055pub fn generate_auth_cookie() -> Result<(String, String)> {
1057 use rand::Rng;
1058
1059 let cookie_path = get_auth_cookie_path();
1060
1061 if let Some(parent) = cookie_path.parent() {
1063 fs::create_dir_all(parent)?;
1064 }
1065
1066 let mut rng = rand::thread_rng();
1068 let username = format!("htree_{}", rng.gen::<u32>());
1069 let password: String = (0..32)
1070 .map(|_| {
1071 let idx = rng.gen_range(0..62);
1072 match idx {
1073 0..=25 => (b'a' + idx) as char,
1074 26..=51 => (b'A' + (idx - 26)) as char,
1075 _ => (b'0' + (idx - 52)) as char,
1076 }
1077 })
1078 .collect();
1079
1080 let content = format!("{}:{}", username, password);
1082 fs::write(&cookie_path, content)?;
1083
1084 #[cfg(unix)]
1086 {
1087 use std::os::unix::fs::PermissionsExt;
1088 let perms = fs::Permissions::from_mode(0o600);
1089 fs::set_permissions(&cookie_path, perms)?;
1090 }
1091
1092 Ok((username, password))
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097 use super::*;
1098 use crate::test_support::{test_env_lock, EnvVarGuard};
1099 use tempfile::TempDir;
1100
1101 #[test]
1102 fn test_config_default() {
1103 let config = Config::default();
1104 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1105 assert!(config.server.enable_auth);
1106 assert!(config.server.enable_multicast);
1107 assert_eq!(config.server.multicast_group, "239.255.42.98");
1108 assert_eq!(config.server.multicast_port, 48555);
1109 assert_eq!(config.server.max_multicast_peers, 12);
1110 assert!(!config.server.enable_wifi_aware);
1111 assert_eq!(config.server.max_wifi_aware_peers, 0);
1112 assert!(!config.server.enable_bluetooth);
1113 assert_eq!(config.server.max_bluetooth_peers, 0);
1114 assert!(config.server.public_plaintext_reads);
1115 assert_eq!(config.storage.max_size_gb, 10);
1116 assert!(config.storage.evict_orphans);
1117 assert!(config.nostr.enabled);
1118 assert!(config
1119 .nostr
1120 .relays
1121 .contains(&"wss://temp.iris.to".to_string()));
1122 assert!(config.blossom.enabled);
1123 assert!(!config.blossom.optimistic_uploads);
1124 assert!(config.blossom.replicate_servers.is_empty());
1125 assert_eq!(config.blossom.replicate_queue_mb, 512);
1126 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1127 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1128 assert_eq!(config.nostr.max_write_distance, 3);
1129 assert_eq!(config.nostr.db_max_size_gb, 10);
1130 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1131 assert!(!config.nostr.negentropy_only);
1132 assert_eq!(config.nostr.overmute_threshold, 1.0);
1133 assert_eq!(
1134 config.nostr.mirror_kinds,
1135 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1136 );
1137 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1138 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1139 assert!(config.nostr.history_sync_on_reconnect);
1140 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1141 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1142 assert!(config.nostr.socialgraph_root.is_none());
1143 assert_eq!(
1144 config.nostr.bootstrap_follows,
1145 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1146 );
1147 assert!(!config.server.socialgraph_snapshot_public);
1148 assert!(config.cashu.accepted_mints.is_empty());
1149 assert!(config.cashu.default_mint.is_none());
1150 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1151 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1152 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1153 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1154 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1155 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1156 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1157 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1158 assert_eq!(config.cashu.payment_default_block_threshold, 0);
1159 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1160 }
1161
1162 #[test]
1163 fn test_blossom_optimistic_uploads_deserialize() {
1164 let toml_str = r#"
1165[blossom]
1166optimistic_uploads = true
1167"#;
1168 let config: Config = toml::from_str(toml_str).unwrap();
1169 assert!(config.blossom.optimistic_uploads);
1170 assert!(config.blossom.require_random_untrusted_ingest);
1171 }
1172
1173 #[test]
1174 fn test_blossom_replication_deserialize() {
1175 let toml_str = r#"
1176[blossom]
1177replicate_servers = ["http://127.0.0.1:8081"]
1178replicate_queue_mb = 128
1179"#;
1180 let config: Config = toml::from_str(toml_str).unwrap();
1181 assert_eq!(config.blossom.replicate_servers, ["http://127.0.0.1:8081"]);
1182 assert_eq!(config.blossom.replicate_queue_mb, 128);
1183 }
1184
1185 #[test]
1186 fn test_server_public_plaintext_reads_deserialize() {
1187 let toml_str = r#"
1188[server]
1189public_plaintext_reads = false
1190"#;
1191 let config: Config = toml::from_str(toml_str).unwrap();
1192 assert!(!config.server.public_plaintext_reads);
1193 assert!(config.server.public_writes);
1194 }
1195
1196 #[test]
1197 fn test_nostr_config_deserialize_with_defaults() {
1198 let toml_str = r#"
1199[nostr]
1200relays = ["wss://relay.damus.io"]
1201"#;
1202 let config: Config = toml::from_str(toml_str).unwrap();
1203 assert!(config.nostr.enabled);
1204 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1205 assert!(config.storage.evict_orphans);
1206 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1207 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1208 assert_eq!(config.nostr.max_write_distance, 3);
1209 assert_eq!(config.nostr.db_max_size_gb, 10);
1210 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1211 assert!(!config.nostr.negentropy_only);
1212 assert_eq!(config.nostr.overmute_threshold, 1.0);
1213 assert_eq!(
1214 config.nostr.mirror_kinds,
1215 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1216 );
1217 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1218 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1219 assert!(config.nostr.history_sync_on_reconnect);
1220 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1221 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1222 assert!(config.nostr.socialgraph_root.is_none());
1223 assert_eq!(
1224 config.nostr.bootstrap_follows,
1225 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1226 );
1227 }
1228
1229 #[test]
1230 fn test_nostr_config_deserialize_with_socialgraph() {
1231 let toml_str = r#"
1232[nostr]
1233relays = ["wss://relay.damus.io"]
1234socialgraph_root = "npub1test"
1235bootstrap_follows = []
1236social_graph_crawl_depth = 3
1237mirror_max_follow_distance = 2
1238max_write_distance = 5
1239negentropy_only = true
1240overmute_threshold = 2.5
1241mirror_kinds = [0, 10000]
1242history_sync_author_chunk_size = 250
1243history_sync_per_author_event_limit = 128
1244history_sync_on_reconnect = false
1245full_text_note_history_follow_distance = 1
1246full_text_note_history_max_relay_pages = 64
1247"#;
1248 let config: Config = toml::from_str(toml_str).unwrap();
1249 assert!(config.nostr.enabled);
1250 assert!(config.storage.evict_orphans);
1251 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1252 assert!(config.nostr.bootstrap_follows.is_empty());
1253 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1254 assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1255 assert_eq!(config.nostr.max_write_distance, 5);
1256 assert_eq!(config.nostr.db_max_size_gb, 10);
1257 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1258 assert!(config.nostr.negentropy_only);
1259 assert_eq!(config.nostr.overmute_threshold, 2.5);
1260 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1261 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1262 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1263 assert!(!config.nostr.history_sync_on_reconnect);
1264 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1265 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1266 }
1267
1268 #[test]
1269 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1270 let toml_str = r#"
1271[nostr]
1272relays = ["wss://relay.damus.io"]
1273crawl_depth = 4
1274"#;
1275 let config: Config = toml::from_str(toml_str).unwrap();
1276 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1277 }
1278
1279 #[test]
1280 fn test_storage_config_disables_orphan_eviction_when_requested() {
1281 let toml_str = r#"
1282[storage]
1283evict_orphans = false
1284"#;
1285 let config: Config = toml::from_str(toml_str).unwrap();
1286 assert!(!config.storage.evict_orphans);
1287 }
1288
1289 #[test]
1290 fn test_server_config_deserialize_with_multicast() {
1291 let toml_str = r#"
1292[server]
1293enable_multicast = true
1294multicast_group = "239.255.42.99"
1295multicast_port = 49001
1296max_multicast_peers = 12
1297enable_wifi_aware = true
1298max_wifi_aware_peers = 5
1299enable_bluetooth = true
1300max_bluetooth_peers = 6
1301"#;
1302 let config: Config = toml::from_str(toml_str).unwrap();
1303 assert!(config.server.enable_multicast);
1304 assert_eq!(config.server.multicast_group, "239.255.42.99");
1305 assert_eq!(config.server.multicast_port, 49_001);
1306 assert_eq!(config.server.max_multicast_peers, 12);
1307 assert!(config.server.enable_wifi_aware);
1308 assert_eq!(config.server.max_wifi_aware_peers, 5);
1309 assert!(config.server.enable_bluetooth);
1310 assert_eq!(config.server.max_bluetooth_peers, 6);
1311 }
1312
1313 #[test]
1314 fn test_cashu_config_deserialize_with_accepted_mints() {
1315 let toml_str = r#"
1316[cashu]
1317accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1318default_mint = "https://mint1.example"
1319quote_payment_offer_sat = 5
1320quote_ttl_ms = 2500
1321settlement_timeout_ms = 7000
1322mint_failure_block_threshold = 3
1323peer_suggested_mint_base_cap_sat = 4
1324peer_suggested_mint_success_step_sat = 2
1325peer_suggested_mint_receipt_step_sat = 3
1326peer_suggested_mint_max_cap_sat = 34
1327payment_default_block_threshold = 2
1328chunk_target_bytes = 65536
1329"#;
1330 let config: Config = toml::from_str(toml_str).unwrap();
1331 assert_eq!(
1332 config.cashu.accepted_mints,
1333 vec![
1334 "https://mint1.example".to_string(),
1335 "http://127.0.0.1:3338".to_string()
1336 ]
1337 );
1338 assert_eq!(
1339 config.cashu.default_mint,
1340 Some("https://mint1.example".to_string())
1341 );
1342 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1343 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1344 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1345 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1346 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1347 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1348 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1349 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1350 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1351 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1352 }
1353
1354 #[test]
1355 fn test_auth_cookie_generation() -> Result<()> {
1356 let _lock = test_env_lock()
1357 .lock()
1358 .unwrap_or_else(|err| err.into_inner());
1359 let temp_dir = TempDir::new()?;
1360 let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1361
1362 let (username, password) = generate_auth_cookie()?;
1363
1364 assert!(username.starts_with("htree_"));
1365 assert_eq!(password.len(), 32);
1366
1367 let cookie_path = get_auth_cookie_path();
1369 assert!(cookie_path.exists());
1370
1371 let (u2, p2) = read_auth_cookie()?;
1373 assert_eq!(username, u2);
1374 assert_eq!(password, p2);
1375
1376 Ok(())
1377 }
1378
1379 #[test]
1380 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1381 let mut config = BlossomConfig::default();
1382 config.servers = vec!["https://legacy.server".to_string()];
1383
1384 let read = config.all_read_servers();
1385 assert!(read.contains(&"https://legacy.server".to_string()));
1386 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1387 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1388 assert!(read.contains(&"https://upload.iris.to".to_string()));
1389
1390 let write = config.all_write_servers();
1391 assert!(write.contains(&"https://legacy.server".to_string()));
1392 assert!(write.contains(&"https://upload.iris.to".to_string()));
1393 }
1394
1395 #[test]
1396 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1397 let config = BlossomConfig {
1398 enabled: true,
1399 servers: Vec::new(),
1400 read_servers: Vec::new(),
1401 write_servers: Vec::new(),
1402 max_upload_mb: default_max_upload_mb(),
1403 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1404 optimistic_uploads: default_optimistic_uploads(),
1405 replicate_servers: Vec::new(),
1406 replicate_queue_mb: default_replicate_queue_mb(),
1407 };
1408
1409 let read = config.all_read_servers();
1410 let mut expected = default_read_servers();
1411 expected.extend(default_write_servers());
1412 expected.sort();
1413 expected.dedup();
1414 assert_eq!(read, expected);
1415
1416 let write = config.all_write_servers();
1417 assert_eq!(write, default_write_servers());
1418 }
1419
1420 #[test]
1421 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1422 let nostr = NostrConfig {
1423 enabled: false,
1424 relays: vec!["wss://relay.example".to_string()],
1425 ..NostrConfig::default()
1426 };
1427 assert!(nostr.active_relays().is_empty());
1428
1429 let blossom = BlossomConfig {
1430 enabled: false,
1431 servers: vec!["https://legacy.server".to_string()],
1432 read_servers: vec!["https://read.example".to_string()],
1433 write_servers: vec!["https://write.example".to_string()],
1434 max_upload_mb: default_max_upload_mb(),
1435 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1436 optimistic_uploads: default_optimistic_uploads(),
1437 replicate_servers: Vec::new(),
1438 replicate_queue_mb: default_replicate_queue_mb(),
1439 };
1440 assert!(blossom.all_read_servers().is_empty());
1441 assert!(blossom.all_write_servers().is_empty());
1442 }
1443
1444 #[test]
1445 fn server_defaults_enable_fips_udp_and_webrtc() {
1446 let server = ServerConfig::default();
1447
1448 assert!(server.enable_fips);
1449 assert!(server.enable_fips_udp);
1450 assert!(server.fips_udp_bind_addr.is_none());
1451 assert!(!server.fips_udp_public);
1452 assert!(server.fips_udp_external_addr.is_none());
1453 assert!(server.enable_fips_webrtc);
1454 assert!(server.fetch_from_fips_peers);
1455 assert!(server.fips_relays.is_empty());
1456 assert!(server.fips_peers.is_empty());
1457 assert_eq!(server.fips_discovery_scope, "hashtree-v1");
1458 assert_eq!(server.fips_request_timeout_ms, 5_500);
1459 }
1460
1461 #[test]
1462 fn server_config_reads_fips_overrides() {
1463 let config: Config = toml::from_str(
1464 r#"
1465[server]
1466enable_fips = true
1467fips_discovery_scope = "test-hashtree"
1468fips_relays = ["wss://fips.example"]
1469fips_peers = [
1470 { npub = "npub1origin", udp_addresses = ["udp:192.0.2.10:2121"] },
1471 { npub = "npub1cache" },
1472]
1473enable_fips_udp = false
1474fips_udp_bind_addr = "0.0.0.0:2121"
1475fips_udp_public = true
1476fips_udp_external_addr = "198.19.77.10:2121"
1477enable_fips_webrtc = true
1478fetch_from_fips_peers = false
1479fips_request_timeout_ms = 42
1480"#,
1481 )
1482 .unwrap();
1483
1484 assert!(config.server.enable_fips);
1485 assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1486 assert_eq!(config.server.fips_relays, ["wss://fips.example"]);
1487 assert_eq!(
1488 config.server.fips_peers,
1489 [
1490 ConfiguredFipsPeer {
1491 npub: "npub1origin".to_string(),
1492 udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
1493 },
1494 ConfiguredFipsPeer {
1495 npub: "npub1cache".to_string(),
1496 udp_addresses: Vec::new(),
1497 },
1498 ]
1499 );
1500 assert!(!config.server.enable_fips_udp);
1501 assert_eq!(
1502 config.server.fips_udp_bind_addr.as_deref(),
1503 Some("0.0.0.0:2121")
1504 );
1505 assert!(config.server.fips_udp_public);
1506 assert_eq!(
1507 config.server.fips_udp_external_addr.as_deref(),
1508 Some("198.19.77.10:2121")
1509 );
1510 assert!(config.server.enable_fips_webrtc);
1511 assert!(!config.server.fetch_from_fips_peers);
1512 assert_eq!(config.server.fips_request_timeout_ms, 42);
1513 }
1514
1515 #[test]
1516 fn server_config_accepts_legacy_http_fips_fetch_name() {
1517 let config: Config = toml::from_str(
1518 r#"
1519[server]
1520http_fips_fetch = false
1521"#,
1522 )
1523 .unwrap();
1524
1525 assert!(!config.server.fetch_from_fips_peers);
1526 }
1527
1528 #[test]
1529 fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1530 let active_nostr = vec!["wss://nostr.example".to_string()];
1531 let mut server = ServerConfig::default();
1532
1533 assert_eq!(
1534 server.resolved_fips_relays(&active_nostr),
1535 [
1536 "wss://nostr.example",
1537 "wss://temp.iris.to",
1538 "wss://relay.primal.net"
1539 ]
1540 );
1541
1542 server.fips_relays = vec!["wss://fips.example".to_string()];
1543 assert_eq!(
1544 server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1545 [
1546 "wss://fips.example",
1547 "wss://temp.iris.to",
1548 "wss://relay.primal.net"
1549 ]
1550 );
1551 }
1552
1553 #[test]
1554 fn fips_relay_resolution_dedupes_bootstrap_relays() {
1555 let mut server = ServerConfig::default();
1556 server.fips_relays = vec![
1557 "wss://temp.iris.to/".to_string(),
1558 " wss://relay.primal.net ".to_string(),
1559 "wss://extra.example".to_string(),
1560 ];
1561
1562 assert_eq!(
1563 server.resolved_fips_relays(&[]),
1564 [
1565 "wss://temp.iris.to",
1566 "wss://relay.primal.net",
1567 "wss://extra.example"
1568 ]
1569 );
1570 }
1571}