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