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