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