1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(default)]
11 pub server: ServerConfig,
12 #[serde(default)]
13 pub storage: StorageConfig,
14 #[serde(default)]
15 pub nostr: NostrConfig,
16 #[serde(default)]
17 pub blossom: BlossomConfig,
18 #[serde(default)]
19 pub sync: SyncConfig,
20 #[serde(default)]
21 pub cashu: CashuConfig,
22 #[serde(default)]
23 pub updater: UpdaterConfig,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct UpdaterConfig {
33 #[serde(default = "default_auto_check")]
34 pub auto_check: bool,
35 #[serde(default)]
36 pub auto_install: bool,
37 #[serde(default = "default_check_interval_hours")]
38 pub check_interval_hours: u32,
39}
40
41impl Default for UpdaterConfig {
42 fn default() -> Self {
43 Self {
44 auto_check: default_auto_check(),
45 auto_install: false,
46 check_interval_hours: default_check_interval_hours(),
47 }
48 }
49}
50
51fn default_auto_check() -> bool {
52 true
53}
54
55fn default_check_interval_hours() -> u32 {
56 24
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "kebab-case")]
61pub enum ServerMode {
62 #[default]
63 Normal,
64 #[serde(alias = "signal-only")]
65 Assist,
66}
67
68impl ServerMode {
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Normal => "normal",
72 Self::Assist => "assist",
73 }
74 }
75
76 pub const fn hash_get_enabled(self) -> bool {
77 matches!(self, Self::Normal)
78 }
79
80 pub const fn background_services_enabled(self) -> bool {
81 matches!(self, Self::Normal)
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ServerConfig {
87 #[serde(default)]
88 pub mode: ServerMode,
89 #[serde(default = "default_bind_address")]
90 pub bind_address: String,
91 #[serde(default = "default_enable_auth")]
92 pub enable_auth: bool,
93 #[serde(default = "default_stun_port")]
95 pub stun_port: u16,
96 #[serde(default = "default_enable_webrtc")]
98 pub enable_webrtc: bool,
99 #[serde(default = "default_enable_fips")]
101 pub enable_fips: bool,
102 #[serde(default = "default_fips_discovery_scope")]
104 pub fips_discovery_scope: String,
105 #[serde(default)]
108 pub fips_relays: Vec<String>,
109 #[serde(default = "default_enable_fips_udp")]
111 pub enable_fips_udp: bool,
112 #[serde(default)]
114 pub fips_udp_bind_addr: Option<String>,
115 #[serde(default)]
117 pub fips_udp_public: bool,
118 #[serde(default)]
120 pub fips_udp_external_addr: Option<String>,
121 #[serde(default = "default_enable_fips_webrtc")]
123 pub enable_fips_webrtc: bool,
124 #[serde(default = "default_fetch_from_fips_peers", alias = "http_fips_fetch")]
126 pub fetch_from_fips_peers: bool,
127 #[serde(default = "default_fips_request_timeout_ms")]
129 pub fips_request_timeout_ms: u64,
130 #[serde(default = "default_http_webrtc_fetch")]
132 pub http_webrtc_fetch: bool,
133 #[serde(default, alias = "peer_direct_urls", alias = "peer_advertise_urls")]
136 pub peer_signal_urls: Vec<String>,
137 #[serde(default = "default_enable_multicast")]
139 pub enable_multicast: bool,
140 #[serde(default = "default_multicast_group")]
142 pub multicast_group: String,
143 #[serde(default = "default_multicast_port")]
145 pub multicast_port: u16,
146 #[serde(default = "default_max_multicast_peers")]
149 pub max_multicast_peers: usize,
150 #[serde(default = "default_enable_wifi_aware")]
152 pub enable_wifi_aware: bool,
153 #[serde(default = "default_max_wifi_aware_peers")]
156 pub max_wifi_aware_peers: usize,
157 #[serde(default = "default_enable_bluetooth")]
159 pub enable_bluetooth: bool,
160 #[serde(default = "default_max_bluetooth_peers")]
163 pub max_bluetooth_peers: usize,
164 #[serde(default = "default_public_writes")]
167 pub public_writes: bool,
168 #[serde(default = "default_socialgraph_snapshot_public")]
170 pub socialgraph_snapshot_public: bool,
171}
172
173fn default_public_writes() -> bool {
174 true
175}
176
177fn default_socialgraph_snapshot_public() -> bool {
178 false
179}
180
181impl ServerConfig {
182 pub fn resolved_fips_relays(&self, active_nostr_relays: &[String]) -> Vec<String> {
183 let configured = if self.fips_relays.is_empty() {
184 active_nostr_relays
185 } else {
186 &self.fips_relays
187 };
188 merge_fips_signal_relays(configured)
189 }
190}
191
192const DEFAULT_FIPS_SIGNAL_RELAYS: [&str; 2] = ["wss://temp.iris.to", "wss://relay.primal.net"];
193
194fn merge_fips_signal_relays(configured: &[String]) -> Vec<String> {
195 let mut relays = Vec::new();
196 for relay in configured
197 .iter()
198 .map(String::as_str)
199 .chain(DEFAULT_FIPS_SIGNAL_RELAYS)
200 {
201 let normalized = relay.trim().trim_end_matches('/').to_string();
202 if normalized.is_empty() || relays.contains(&normalized) {
203 continue;
204 }
205 relays.push(normalized);
206 }
207 relays
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct StorageConfig {
212 #[serde(default = "default_data_dir")]
213 pub data_dir: String,
214 #[serde(default = "default_max_size_gb")]
215 pub max_size_gb: u64,
216 #[serde(default = "default_storage_evict_orphans")]
217 pub evict_orphans: bool,
218 #[serde(default)]
220 pub s3: Option<S3Config>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct S3Config {
226 pub endpoint: String,
228 pub bucket: String,
230 #[serde(default)]
232 pub prefix: Option<String>,
233 #[serde(default = "default_s3_region")]
235 pub region: String,
236 #[serde(default)]
238 pub access_key: Option<String>,
239 #[serde(default)]
241 pub secret_key: Option<String>,
242 #[serde(default)]
244 pub public_url: Option<String>,
245}
246
247fn default_s3_region() -> String {
248 "auto".to_string()
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct NostrConfig {
253 #[serde(default = "default_nostr_enabled")]
254 pub enabled: bool,
255 #[serde(default = "default_relays")]
256 pub relays: Vec<String>,
257 #[serde(default)]
259 pub allowed_npubs: Vec<String>,
260 #[serde(default)]
262 pub socialgraph_root: Option<String>,
263 #[serde(default = "default_nostr_bootstrap_follows")]
266 pub bootstrap_follows: Vec<String>,
267 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
269 pub social_graph_crawl_depth: u32,
270 #[serde(default)]
273 pub mirror_max_follow_distance: Option<u32>,
274 #[serde(default = "default_max_write_distance")]
276 pub max_write_distance: u32,
277 #[serde(default = "default_nostr_db_max_size_gb")]
279 pub db_max_size_gb: u64,
280 #[serde(default = "default_nostr_spambox_max_size_gb")]
283 pub spambox_max_size_gb: u64,
284 #[serde(default)]
286 pub negentropy_only: bool,
287 #[serde(default = "default_nostr_overmute_threshold")]
289 pub overmute_threshold: f64,
290 #[serde(default = "default_nostr_mirror_kinds")]
292 pub mirror_kinds: Vec<u16>,
293 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
295 pub history_sync_author_chunk_size: usize,
296 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
298 pub history_sync_per_author_event_limit: usize,
299 #[serde(default = "default_nostr_history_sync_on_reconnect")]
301 pub history_sync_on_reconnect: bool,
302 #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
305 pub full_text_note_history_follow_distance: Option<u32>,
306 #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
309 pub full_text_note_history_max_relay_pages: usize,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct BlossomConfig {
314 #[serde(default = "default_blossom_enabled")]
315 pub enabled: bool,
316 #[serde(default)]
318 pub servers: Vec<String>,
319 #[serde(default = "default_read_servers")]
321 pub read_servers: Vec<String>,
322 #[serde(default = "default_write_servers")]
324 pub write_servers: Vec<String>,
325 #[serde(default = "default_max_upload_mb")]
327 pub max_upload_mb: u64,
328 #[serde(default = "default_require_random_untrusted_ingest")]
330 pub require_random_untrusted_ingest: bool,
331 #[serde(default = "default_optimistic_uploads")]
334 pub optimistic_uploads: bool,
335}
336
337impl BlossomConfig {
338 pub fn all_read_servers(&self) -> Vec<String> {
339 if !self.enabled {
340 return Vec::new();
341 }
342 let mut servers = self.servers.clone();
343 servers.extend(self.read_servers.clone());
344 servers.extend(self.write_servers.clone());
345 if servers.is_empty() {
346 servers = default_read_servers();
347 servers.extend(default_write_servers());
348 }
349 servers.sort();
350 servers.dedup();
351 servers
352 }
353
354 pub fn all_write_servers(&self) -> Vec<String> {
355 if !self.enabled {
356 return Vec::new();
357 }
358 let mut servers = self.servers.clone();
359 servers.extend(self.write_servers.clone());
360 if servers.is_empty() {
361 servers = default_write_servers();
362 }
363 servers.sort();
364 servers.dedup();
365 servers
366 }
367}
368
369impl NostrConfig {
370 pub fn active_relays(&self) -> Vec<String> {
371 if self.enabled {
372 self.relays.clone()
373 } else {
374 Vec::new()
375 }
376 }
377}
378
379fn default_read_servers() -> Vec<String> {
381 let mut servers = vec![
382 "https://blossom.primal.net".to_string(),
383 "https://cdn.iris.to".to_string(),
384 ];
385 servers.sort();
386 servers
387}
388
389fn default_write_servers() -> Vec<String> {
390 vec!["https://upload.iris.to".to_string()]
391}
392
393fn default_max_upload_mb() -> u64 {
394 5
395}
396
397fn default_require_random_untrusted_ingest() -> bool {
398 true
399}
400
401fn default_optimistic_uploads() -> bool {
402 false
403}
404
405fn default_nostr_enabled() -> bool {
406 true
407}
408
409fn default_blossom_enabled() -> bool {
410 true
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct SyncConfig {
415 #[serde(default = "default_sync_enabled")]
417 pub enabled: bool,
418 #[serde(default = "default_sync_own")]
420 pub sync_own: bool,
421 #[serde(default = "default_sync_followed")]
423 pub sync_followed: bool,
424 #[serde(default = "default_max_concurrent")]
426 pub max_concurrent: usize,
427 #[serde(default = "default_webrtc_timeout_ms")]
429 pub webrtc_timeout_ms: u64,
430 #[serde(default = "default_blossom_timeout_ms")]
432 pub blossom_timeout_ms: u64,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct CashuConfig {
437 #[serde(default)]
439 pub accepted_mints: Vec<String>,
440 #[serde(default)]
442 pub default_mint: Option<String>,
443 #[serde(default = "default_cashu_quote_payment_offer_sat")]
445 pub quote_payment_offer_sat: u64,
446 #[serde(default = "default_cashu_quote_ttl_ms")]
448 pub quote_ttl_ms: u32,
449 #[serde(default = "default_cashu_settlement_timeout_ms")]
451 pub settlement_timeout_ms: u64,
452 #[serde(default = "default_cashu_mint_failure_block_threshold")]
454 pub mint_failure_block_threshold: u64,
455 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
457 pub peer_suggested_mint_base_cap_sat: u64,
458 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
460 pub peer_suggested_mint_success_step_sat: u64,
461 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
463 pub peer_suggested_mint_receipt_step_sat: u64,
464 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
466 pub peer_suggested_mint_max_cap_sat: u64,
467 #[serde(default)]
469 pub payment_default_block_threshold: u64,
470 #[serde(default = "default_cashu_chunk_target_bytes")]
472 pub chunk_target_bytes: usize,
473}
474
475impl Default for CashuConfig {
476 fn default() -> Self {
477 Self {
478 accepted_mints: Vec::new(),
479 default_mint: None,
480 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
481 quote_ttl_ms: default_cashu_quote_ttl_ms(),
482 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
483 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
484 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
485 peer_suggested_mint_success_step_sat:
486 default_cashu_peer_suggested_mint_success_step_sat(),
487 peer_suggested_mint_receipt_step_sat:
488 default_cashu_peer_suggested_mint_receipt_step_sat(),
489 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
490 payment_default_block_threshold: 0,
491 chunk_target_bytes: default_cashu_chunk_target_bytes(),
492 }
493 }
494}
495
496fn default_cashu_quote_payment_offer_sat() -> u64 {
497 3
498}
499
500fn default_cashu_quote_ttl_ms() -> u32 {
501 1_500
502}
503
504fn default_cashu_settlement_timeout_ms() -> u64 {
505 5_000
506}
507
508fn default_cashu_mint_failure_block_threshold() -> u64 {
509 2
510}
511
512fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
513 3
514}
515
516fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
517 1
518}
519
520fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
521 2
522}
523
524fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
525 21
526}
527
528fn default_cashu_chunk_target_bytes() -> usize {
529 32 * 1024
530}
531
532fn default_sync_enabled() -> bool {
533 true
534}
535
536fn default_sync_own() -> bool {
537 true
538}
539
540fn default_sync_followed() -> bool {
541 true
542}
543
544fn default_max_concurrent() -> usize {
545 3
546}
547
548fn default_webrtc_timeout_ms() -> u64 {
549 2000
550}
551
552fn default_blossom_timeout_ms() -> u64 {
553 10000
554}
555
556fn default_social_graph_crawl_depth() -> u32 {
557 2
558}
559
560fn default_nostr_bootstrap_follows() -> Vec<String> {
561 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
562}
563
564fn default_max_write_distance() -> u32 {
565 3
566}
567
568fn default_nostr_db_max_size_gb() -> u64 {
569 10
570}
571
572fn default_nostr_spambox_max_size_gb() -> u64 {
573 1
574}
575
576fn default_nostr_history_sync_on_reconnect() -> bool {
577 true
578}
579
580fn default_nostr_overmute_threshold() -> f64 {
581 1.0
582}
583
584fn default_nostr_mirror_kinds() -> Vec<u16> {
585 vec![0, 1, 3, 6, 7, 9_735, 30_023]
586}
587
588fn default_nostr_history_sync_author_chunk_size() -> usize {
589 5_000
590}
591
592fn default_nostr_history_sync_per_author_event_limit() -> usize {
593 256
594}
595
596fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
597 Some(2)
598}
599
600fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
601 0
602}
603
604fn default_relays() -> Vec<String> {
605 vec![
606 "wss://relay.damus.io".to_string(),
607 "wss://relay.snort.social".to_string(),
608 "wss://temp.iris.to".to_string(),
609 "wss://upload.iris.to/nostr".to_string(),
610 ]
611}
612
613fn default_bind_address() -> String {
614 "127.0.0.1:8080".to_string()
615}
616
617fn default_enable_auth() -> bool {
618 true
619}
620
621fn default_stun_port() -> u16 {
622 3478 }
624
625fn default_enable_webrtc() -> bool {
626 true
627}
628
629fn default_enable_fips() -> bool {
630 true
631}
632
633fn default_fips_discovery_scope() -> String {
634 "hashtree-v1".to_string()
635}
636
637fn default_enable_fips_udp() -> bool {
638 true
639}
640
641fn default_enable_fips_webrtc() -> bool {
642 true
643}
644
645fn default_fetch_from_fips_peers() -> bool {
646 true
647}
648
649fn default_fips_request_timeout_ms() -> u64 {
650 5_500
651}
652
653fn default_http_webrtc_fetch() -> bool {
654 true
655}
656
657fn default_enable_multicast() -> bool {
658 true
659}
660
661fn default_multicast_group() -> String {
662 "239.255.42.98".to_string()
663}
664
665fn default_multicast_port() -> u16 {
666 48555
667}
668
669fn default_max_multicast_peers() -> usize {
670 12
671}
672
673fn default_enable_wifi_aware() -> bool {
674 false
675}
676
677fn default_max_wifi_aware_peers() -> usize {
678 0
679}
680
681fn default_enable_bluetooth() -> bool {
682 false
683}
684
685fn default_max_bluetooth_peers() -> usize {
686 0
687}
688
689fn default_data_dir() -> String {
690 hashtree_config::get_hashtree_dir()
691 .join("data")
692 .to_string_lossy()
693 .to_string()
694}
695
696fn default_max_size_gb() -> u64 {
697 10
698}
699
700fn default_storage_evict_orphans() -> bool {
701 true
702}
703
704impl Default for ServerConfig {
705 fn default() -> Self {
706 Self {
707 mode: ServerMode::default(),
708 bind_address: default_bind_address(),
709 enable_auth: default_enable_auth(),
710 stun_port: default_stun_port(),
711 enable_webrtc: default_enable_webrtc(),
712 enable_fips: default_enable_fips(),
713 fips_discovery_scope: default_fips_discovery_scope(),
714 fips_relays: Vec::new(),
715 enable_fips_udp: default_enable_fips_udp(),
716 fips_udp_bind_addr: None,
717 fips_udp_public: false,
718 fips_udp_external_addr: None,
719 enable_fips_webrtc: default_enable_fips_webrtc(),
720 fetch_from_fips_peers: default_fetch_from_fips_peers(),
721 fips_request_timeout_ms: default_fips_request_timeout_ms(),
722 http_webrtc_fetch: default_http_webrtc_fetch(),
723 peer_signal_urls: Vec::new(),
724 enable_multicast: default_enable_multicast(),
725 multicast_group: default_multicast_group(),
726 multicast_port: default_multicast_port(),
727 max_multicast_peers: default_max_multicast_peers(),
728 enable_wifi_aware: default_enable_wifi_aware(),
729 max_wifi_aware_peers: default_max_wifi_aware_peers(),
730 enable_bluetooth: default_enable_bluetooth(),
731 max_bluetooth_peers: default_max_bluetooth_peers(),
732 public_writes: default_public_writes(),
733 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
734 }
735 }
736}
737
738impl Default for StorageConfig {
739 fn default() -> Self {
740 Self {
741 data_dir: default_data_dir(),
742 max_size_gb: default_max_size_gb(),
743 evict_orphans: default_storage_evict_orphans(),
744 s3: None,
745 }
746 }
747}
748
749impl Default for NostrConfig {
750 fn default() -> Self {
751 Self {
752 enabled: default_nostr_enabled(),
753 relays: default_relays(),
754 allowed_npubs: Vec::new(),
755 socialgraph_root: None,
756 bootstrap_follows: default_nostr_bootstrap_follows(),
757 social_graph_crawl_depth: default_social_graph_crawl_depth(),
758 mirror_max_follow_distance: None,
759 max_write_distance: default_max_write_distance(),
760 db_max_size_gb: default_nostr_db_max_size_gb(),
761 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
762 negentropy_only: false,
763 overmute_threshold: default_nostr_overmute_threshold(),
764 mirror_kinds: default_nostr_mirror_kinds(),
765 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
766 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
767 ),
768 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
769 full_text_note_history_follow_distance:
770 default_nostr_full_text_note_history_follow_distance(),
771 full_text_note_history_max_relay_pages:
772 default_nostr_full_text_note_history_max_relay_pages(),
773 }
774 }
775}
776
777impl Default for BlossomConfig {
778 fn default() -> Self {
779 Self {
780 enabled: default_blossom_enabled(),
781 servers: Vec::new(),
782 read_servers: default_read_servers(),
783 write_servers: default_write_servers(),
784 max_upload_mb: default_max_upload_mb(),
785 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
786 optimistic_uploads: default_optimistic_uploads(),
787 }
788 }
789}
790
791impl Default for SyncConfig {
792 fn default() -> Self {
793 Self {
794 enabled: default_sync_enabled(),
795 sync_own: default_sync_own(),
796 sync_followed: default_sync_followed(),
797 max_concurrent: default_max_concurrent(),
798 webrtc_timeout_ms: default_webrtc_timeout_ms(),
799 blossom_timeout_ms: default_blossom_timeout_ms(),
800 }
801 }
802}
803
804impl Config {
805 pub fn load() -> Result<Self> {
807 let config_path = get_config_path();
808
809 if config_path.exists() {
810 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
811 toml::from_str(&content).context("Failed to parse config file")
812 } else {
813 let config = Config::default();
814 config.save()?;
815 Ok(config)
816 }
817 }
818
819 pub fn save(&self) -> Result<()> {
821 let config_path = get_config_path();
822
823 if let Some(parent) = config_path.parent() {
825 fs::create_dir_all(parent)?;
826 }
827
828 let content = toml::to_string_pretty(self)?;
829 fs::write(&config_path, content)?;
830
831 Ok(())
832 }
833}
834
835pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
837
838fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
839 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
840 let entries = hashtree_config::parse_keys_file(&content);
841 let nsec_str = entries
842 .into_iter()
843 .next()
844 .map(|e| e.secret)
845 .context("Keys file is empty")?;
846 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
847 Ok(Keys::new(secret_key))
848}
849
850fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
851 if let (Some(data_dir), Some(config)) = (data_dir, config) {
852 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
853 }
854}
855
856fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
857 if let Some(parent) = keys_path.parent() {
858 fs::create_dir_all(parent)?;
859 }
860
861 let nsec = keys
862 .secret_key()
863 .to_bech32()
864 .context("Failed to encode nsec")?;
865 fs::write(keys_path, &nsec)?;
866
867 #[cfg(unix)]
868 {
869 use std::os::unix::fs::PermissionsExt;
870 let perms = fs::Permissions::from_mode(0o600);
871 fs::set_permissions(keys_path, perms)?;
872 }
873
874 Ok(())
875}
876
877pub fn ensure_auth_cookie() -> Result<(String, String)> {
879 let cookie_path = get_auth_cookie_path();
880
881 if cookie_path.exists() {
882 read_auth_cookie()
883 } else {
884 generate_auth_cookie()
885 }
886}
887
888pub fn read_auth_cookie() -> Result<(String, String)> {
890 let cookie_path = get_auth_cookie_path();
891 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
892
893 let parts: Vec<&str> = content.trim().split(':').collect();
894 if parts.len() != 2 {
895 anyhow::bail!("Invalid auth cookie format");
896 }
897
898 Ok((parts[0].to_string(), parts[1].to_string()))
899}
900
901pub fn ensure_keys() -> Result<(Keys, bool)> {
904 let config_dir = get_hashtree_dir();
905 let config = Config::load().ok();
906 let data_dir = config
907 .as_ref()
908 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
909 ensure_keys_in(&config_dir, data_dir, config.as_ref())
910}
911
912pub fn ensure_keys_in(
915 config_dir: &Path,
916 data_dir: Option<&Path>,
917 config: Option<&Config>,
918) -> Result<(Keys, bool)> {
919 let keys_path = config_dir.join("keys");
920
921 if keys_path.exists() {
922 Ok((read_keys_from_path(&keys_path)?, false))
923 } else {
924 let keys = generate_keys_in(config_dir, data_dir, config)?;
925 Ok((keys, true))
926 }
927}
928
929pub fn read_keys() -> Result<Keys> {
931 read_keys_in(&get_hashtree_dir())
932}
933
934pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
936 read_keys_from_path(&config_dir.join("keys"))
937}
938
939pub fn ensure_keys_string() -> Result<(String, bool)> {
942 let config_dir = get_hashtree_dir();
943 let config = Config::load().ok();
944 let data_dir = config
945 .as_ref()
946 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
947 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
948}
949
950pub fn ensure_keys_string_in(
953 config_dir: &Path,
954 data_dir: Option<&Path>,
955 config: Option<&Config>,
956) -> Result<(String, bool)> {
957 let keys_path = config_dir.join("keys");
958
959 if keys_path.exists() {
960 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
961 let entries = hashtree_config::parse_keys_file(&content);
962 let nsec_str = entries
963 .into_iter()
964 .next()
965 .map(|e| e.secret)
966 .context("Keys file is empty")?;
967 Ok((nsec_str, false))
968 } else {
969 let keys = generate_keys_in(config_dir, data_dir, config)?;
970 let nsec = keys
971 .secret_key()
972 .to_bech32()
973 .context("Failed to encode nsec")?;
974 Ok((nsec, true))
975 }
976}
977
978pub fn generate_keys() -> Result<Keys> {
980 let config_dir = get_hashtree_dir();
981 let config = Config::load().ok();
982 let data_dir = config
983 .as_ref()
984 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
985 generate_keys_in(&config_dir, data_dir, config.as_ref())
986}
987
988pub fn generate_keys_in(
991 config_dir: &Path,
992 data_dir: Option<&Path>,
993 config: Option<&Config>,
994) -> Result<Keys> {
995 let keys = Keys::generate();
996 write_keys_to_path(&config_dir.join("keys"), &keys)?;
997 seed_identity_defaults_if_needed(data_dir, config);
998 Ok(keys)
999}
1000
1001pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
1003 keys.public_key().to_bytes()
1004}
1005
1006pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
1008 use nostr::PublicKey;
1009 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
1010 Ok(pk.to_bytes())
1011}
1012
1013pub fn generate_auth_cookie() -> Result<(String, String)> {
1015 use rand::Rng;
1016
1017 let cookie_path = get_auth_cookie_path();
1018
1019 if let Some(parent) = cookie_path.parent() {
1021 fs::create_dir_all(parent)?;
1022 }
1023
1024 let mut rng = rand::thread_rng();
1026 let username = format!("htree_{}", rng.gen::<u32>());
1027 let password: String = (0..32)
1028 .map(|_| {
1029 let idx = rng.gen_range(0..62);
1030 match idx {
1031 0..=25 => (b'a' + idx) as char,
1032 26..=51 => (b'A' + (idx - 26)) as char,
1033 _ => (b'0' + (idx - 52)) as char,
1034 }
1035 })
1036 .collect();
1037
1038 let content = format!("{}:{}", username, password);
1040 fs::write(&cookie_path, content)?;
1041
1042 #[cfg(unix)]
1044 {
1045 use std::os::unix::fs::PermissionsExt;
1046 let perms = fs::Permissions::from_mode(0o600);
1047 fs::set_permissions(&cookie_path, perms)?;
1048 }
1049
1050 Ok((username, password))
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use crate::test_support::{test_env_lock, EnvVarGuard};
1057 use tempfile::TempDir;
1058
1059 #[test]
1060 fn test_config_default() {
1061 let config = Config::default();
1062 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1063 assert!(config.server.enable_auth);
1064 assert!(config.server.enable_multicast);
1065 assert_eq!(config.server.multicast_group, "239.255.42.98");
1066 assert_eq!(config.server.multicast_port, 48555);
1067 assert_eq!(config.server.max_multicast_peers, 12);
1068 assert!(!config.server.enable_wifi_aware);
1069 assert_eq!(config.server.max_wifi_aware_peers, 0);
1070 assert!(!config.server.enable_bluetooth);
1071 assert_eq!(config.server.max_bluetooth_peers, 0);
1072 assert_eq!(config.storage.max_size_gb, 10);
1073 assert!(config.storage.evict_orphans);
1074 assert!(config.nostr.enabled);
1075 assert!(config
1076 .nostr
1077 .relays
1078 .contains(&"wss://upload.iris.to/nostr".to_string()));
1079 assert!(config.blossom.enabled);
1080 assert!(!config.blossom.optimistic_uploads);
1081 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1082 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1083 assert_eq!(config.nostr.max_write_distance, 3);
1084 assert_eq!(config.nostr.db_max_size_gb, 10);
1085 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1086 assert!(!config.nostr.negentropy_only);
1087 assert_eq!(config.nostr.overmute_threshold, 1.0);
1088 assert_eq!(
1089 config.nostr.mirror_kinds,
1090 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1091 );
1092 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1093 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1094 assert!(config.nostr.history_sync_on_reconnect);
1095 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1096 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1097 assert!(config.nostr.socialgraph_root.is_none());
1098 assert_eq!(
1099 config.nostr.bootstrap_follows,
1100 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1101 );
1102 assert!(!config.server.socialgraph_snapshot_public);
1103 assert!(config.cashu.accepted_mints.is_empty());
1104 assert!(config.cashu.default_mint.is_none());
1105 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1106 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1107 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1108 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1109 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1110 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1111 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1112 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1113 assert_eq!(config.cashu.payment_default_block_threshold, 0);
1114 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1115 }
1116
1117 #[test]
1118 fn test_blossom_optimistic_uploads_deserialize() {
1119 let toml_str = r#"
1120[blossom]
1121optimistic_uploads = true
1122"#;
1123 let config: Config = toml::from_str(toml_str).unwrap();
1124 assert!(config.blossom.optimistic_uploads);
1125 assert!(config.blossom.require_random_untrusted_ingest);
1126 }
1127
1128 #[test]
1129 fn test_nostr_config_deserialize_with_defaults() {
1130 let toml_str = r#"
1131[nostr]
1132relays = ["wss://relay.damus.io"]
1133"#;
1134 let config: Config = toml::from_str(toml_str).unwrap();
1135 assert!(config.nostr.enabled);
1136 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1137 assert!(config.storage.evict_orphans);
1138 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1139 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1140 assert_eq!(config.nostr.max_write_distance, 3);
1141 assert_eq!(config.nostr.db_max_size_gb, 10);
1142 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1143 assert!(!config.nostr.negentropy_only);
1144 assert_eq!(config.nostr.overmute_threshold, 1.0);
1145 assert_eq!(
1146 config.nostr.mirror_kinds,
1147 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1148 );
1149 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1150 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1151 assert!(config.nostr.history_sync_on_reconnect);
1152 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1153 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1154 assert!(config.nostr.socialgraph_root.is_none());
1155 assert_eq!(
1156 config.nostr.bootstrap_follows,
1157 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1158 );
1159 }
1160
1161 #[test]
1162 fn test_nostr_config_deserialize_with_socialgraph() {
1163 let toml_str = r#"
1164[nostr]
1165relays = ["wss://relay.damus.io"]
1166socialgraph_root = "npub1test"
1167bootstrap_follows = []
1168social_graph_crawl_depth = 3
1169mirror_max_follow_distance = 2
1170max_write_distance = 5
1171negentropy_only = true
1172overmute_threshold = 2.5
1173mirror_kinds = [0, 10000]
1174history_sync_author_chunk_size = 250
1175history_sync_per_author_event_limit = 128
1176history_sync_on_reconnect = false
1177full_text_note_history_follow_distance = 1
1178full_text_note_history_max_relay_pages = 64
1179"#;
1180 let config: Config = toml::from_str(toml_str).unwrap();
1181 assert!(config.nostr.enabled);
1182 assert!(config.storage.evict_orphans);
1183 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1184 assert!(config.nostr.bootstrap_follows.is_empty());
1185 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1186 assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1187 assert_eq!(config.nostr.max_write_distance, 5);
1188 assert_eq!(config.nostr.db_max_size_gb, 10);
1189 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1190 assert!(config.nostr.negentropy_only);
1191 assert_eq!(config.nostr.overmute_threshold, 2.5);
1192 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1193 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1194 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1195 assert!(!config.nostr.history_sync_on_reconnect);
1196 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1197 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1198 }
1199
1200 #[test]
1201 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1202 let toml_str = r#"
1203[nostr]
1204relays = ["wss://relay.damus.io"]
1205crawl_depth = 4
1206"#;
1207 let config: Config = toml::from_str(toml_str).unwrap();
1208 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1209 }
1210
1211 #[test]
1212 fn test_storage_config_disables_orphan_eviction_when_requested() {
1213 let toml_str = r#"
1214[storage]
1215evict_orphans = false
1216"#;
1217 let config: Config = toml::from_str(toml_str).unwrap();
1218 assert!(!config.storage.evict_orphans);
1219 }
1220
1221 #[test]
1222 fn test_server_config_deserialize_with_multicast() {
1223 let toml_str = r#"
1224[server]
1225enable_multicast = true
1226multicast_group = "239.255.42.99"
1227multicast_port = 49001
1228max_multicast_peers = 12
1229enable_wifi_aware = true
1230max_wifi_aware_peers = 5
1231enable_bluetooth = true
1232max_bluetooth_peers = 6
1233"#;
1234 let config: Config = toml::from_str(toml_str).unwrap();
1235 assert!(config.server.enable_multicast);
1236 assert_eq!(config.server.multicast_group, "239.255.42.99");
1237 assert_eq!(config.server.multicast_port, 49_001);
1238 assert_eq!(config.server.max_multicast_peers, 12);
1239 assert!(config.server.enable_wifi_aware);
1240 assert_eq!(config.server.max_wifi_aware_peers, 5);
1241 assert!(config.server.enable_bluetooth);
1242 assert_eq!(config.server.max_bluetooth_peers, 6);
1243 }
1244
1245 #[test]
1246 fn test_cashu_config_deserialize_with_accepted_mints() {
1247 let toml_str = r#"
1248[cashu]
1249accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1250default_mint = "https://mint1.example"
1251quote_payment_offer_sat = 5
1252quote_ttl_ms = 2500
1253settlement_timeout_ms = 7000
1254mint_failure_block_threshold = 3
1255peer_suggested_mint_base_cap_sat = 4
1256peer_suggested_mint_success_step_sat = 2
1257peer_suggested_mint_receipt_step_sat = 3
1258peer_suggested_mint_max_cap_sat = 34
1259payment_default_block_threshold = 2
1260chunk_target_bytes = 65536
1261"#;
1262 let config: Config = toml::from_str(toml_str).unwrap();
1263 assert_eq!(
1264 config.cashu.accepted_mints,
1265 vec![
1266 "https://mint1.example".to_string(),
1267 "http://127.0.0.1:3338".to_string()
1268 ]
1269 );
1270 assert_eq!(
1271 config.cashu.default_mint,
1272 Some("https://mint1.example".to_string())
1273 );
1274 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1275 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1276 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1277 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1278 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1279 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1280 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1281 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1282 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1283 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1284 }
1285
1286 #[test]
1287 fn test_auth_cookie_generation() -> Result<()> {
1288 let _lock = test_env_lock()
1289 .lock()
1290 .unwrap_or_else(|err| err.into_inner());
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 mut config = BlossomConfig::default();
1314 config.servers = vec!["https://legacy.server".to_string()];
1315
1316 let read = config.all_read_servers();
1317 assert!(read.contains(&"https://legacy.server".to_string()));
1318 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1319 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1320 assert!(read.contains(&"https://upload.iris.to".to_string()));
1321
1322 let write = config.all_write_servers();
1323 assert!(write.contains(&"https://legacy.server".to_string()));
1324 assert!(write.contains(&"https://upload.iris.to".to_string()));
1325 }
1326
1327 #[test]
1328 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1329 let config = BlossomConfig {
1330 enabled: true,
1331 servers: Vec::new(),
1332 read_servers: Vec::new(),
1333 write_servers: Vec::new(),
1334 max_upload_mb: default_max_upload_mb(),
1335 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1336 optimistic_uploads: default_optimistic_uploads(),
1337 };
1338
1339 let read = config.all_read_servers();
1340 let mut expected = default_read_servers();
1341 expected.extend(default_write_servers());
1342 expected.sort();
1343 expected.dedup();
1344 assert_eq!(read, expected);
1345
1346 let write = config.all_write_servers();
1347 assert_eq!(write, default_write_servers());
1348 }
1349
1350 #[test]
1351 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1352 let nostr = NostrConfig {
1353 enabled: false,
1354 relays: vec!["wss://relay.example".to_string()],
1355 ..NostrConfig::default()
1356 };
1357 assert!(nostr.active_relays().is_empty());
1358
1359 let blossom = BlossomConfig {
1360 enabled: false,
1361 servers: vec!["https://legacy.server".to_string()],
1362 read_servers: vec!["https://read.example".to_string()],
1363 write_servers: vec!["https://write.example".to_string()],
1364 max_upload_mb: default_max_upload_mb(),
1365 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1366 optimistic_uploads: default_optimistic_uploads(),
1367 };
1368 assert!(blossom.all_read_servers().is_empty());
1369 assert!(blossom.all_write_servers().is_empty());
1370 }
1371
1372 #[test]
1373 fn server_defaults_enable_fips_udp_and_webrtc() {
1374 let server = ServerConfig::default();
1375
1376 assert!(server.enable_fips);
1377 assert!(server.enable_fips_udp);
1378 assert!(server.fips_udp_bind_addr.is_none());
1379 assert!(!server.fips_udp_public);
1380 assert!(server.fips_udp_external_addr.is_none());
1381 assert!(server.enable_fips_webrtc);
1382 assert!(server.fetch_from_fips_peers);
1383 assert!(server.fips_relays.is_empty());
1384 assert_eq!(server.fips_discovery_scope, "hashtree-v1");
1385 assert_eq!(server.fips_request_timeout_ms, 5_500);
1386 }
1387
1388 #[test]
1389 fn server_config_reads_fips_overrides() {
1390 let config: Config = toml::from_str(
1391 r#"
1392[server]
1393enable_fips = true
1394fips_discovery_scope = "test-hashtree"
1395fips_relays = ["wss://fips.example"]
1396enable_fips_udp = false
1397fips_udp_bind_addr = "0.0.0.0:2121"
1398fips_udp_public = true
1399fips_udp_external_addr = "198.19.77.10:2121"
1400enable_fips_webrtc = true
1401fetch_from_fips_peers = false
1402fips_request_timeout_ms = 42
1403"#,
1404 )
1405 .unwrap();
1406
1407 assert!(config.server.enable_fips);
1408 assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1409 assert_eq!(config.server.fips_relays, ["wss://fips.example"]);
1410 assert!(!config.server.enable_fips_udp);
1411 assert_eq!(
1412 config.server.fips_udp_bind_addr.as_deref(),
1413 Some("0.0.0.0:2121")
1414 );
1415 assert!(config.server.fips_udp_public);
1416 assert_eq!(
1417 config.server.fips_udp_external_addr.as_deref(),
1418 Some("198.19.77.10:2121")
1419 );
1420 assert!(config.server.enable_fips_webrtc);
1421 assert!(!config.server.fetch_from_fips_peers);
1422 assert_eq!(config.server.fips_request_timeout_ms, 42);
1423 }
1424
1425 #[test]
1426 fn server_config_accepts_legacy_http_fips_fetch_name() {
1427 let config: Config = toml::from_str(
1428 r#"
1429[server]
1430http_fips_fetch = false
1431"#,
1432 )
1433 .unwrap();
1434
1435 assert!(!config.server.fetch_from_fips_peers);
1436 }
1437
1438 #[test]
1439 fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1440 let active_nostr = vec!["wss://nostr.example".to_string()];
1441 let mut server = ServerConfig::default();
1442
1443 assert_eq!(
1444 server.resolved_fips_relays(&active_nostr),
1445 [
1446 "wss://nostr.example",
1447 "wss://temp.iris.to",
1448 "wss://relay.primal.net"
1449 ]
1450 );
1451
1452 server.fips_relays = vec!["wss://fips.example".to_string()];
1453 assert_eq!(
1454 server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1455 [
1456 "wss://fips.example",
1457 "wss://temp.iris.to",
1458 "wss://relay.primal.net"
1459 ]
1460 );
1461 }
1462
1463 #[test]
1464 fn fips_relay_resolution_dedupes_bootstrap_relays() {
1465 let mut server = ServerConfig::default();
1466 server.fips_relays = vec![
1467 "wss://temp.iris.to/".to_string(),
1468 " wss://relay.primal.net ".to_string(),
1469 "wss://extra.example".to_string(),
1470 ];
1471
1472 assert_eq!(
1473 server.resolved_fips_relays(&[]),
1474 [
1475 "wss://temp.iris.to",
1476 "wss://relay.primal.net",
1477 "wss://extra.example"
1478 ]
1479 );
1480 }
1481}