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