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 if self.fips_relays.is_empty() {
184 active_nostr_relays.to_vec()
185 } else {
186 self.fips_relays.clone()
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct StorageConfig {
193 #[serde(default = "default_data_dir")]
194 pub data_dir: String,
195 #[serde(default = "default_max_size_gb")]
196 pub max_size_gb: u64,
197 #[serde(default = "default_storage_evict_orphans")]
198 pub evict_orphans: bool,
199 #[serde(default)]
201 pub s3: Option<S3Config>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct S3Config {
207 pub endpoint: String,
209 pub bucket: String,
211 #[serde(default)]
213 pub prefix: Option<String>,
214 #[serde(default = "default_s3_region")]
216 pub region: String,
217 #[serde(default)]
219 pub access_key: Option<String>,
220 #[serde(default)]
222 pub secret_key: Option<String>,
223 #[serde(default)]
225 pub public_url: Option<String>,
226}
227
228fn default_s3_region() -> String {
229 "auto".to_string()
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct NostrConfig {
234 #[serde(default = "default_nostr_enabled")]
235 pub enabled: bool,
236 #[serde(default = "default_relays")]
237 pub relays: Vec<String>,
238 #[serde(default)]
240 pub allowed_npubs: Vec<String>,
241 #[serde(default)]
243 pub socialgraph_root: Option<String>,
244 #[serde(default = "default_nostr_bootstrap_follows")]
247 pub bootstrap_follows: Vec<String>,
248 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
250 pub social_graph_crawl_depth: u32,
251 #[serde(default)]
254 pub mirror_max_follow_distance: Option<u32>,
255 #[serde(default = "default_max_write_distance")]
257 pub max_write_distance: u32,
258 #[serde(default = "default_nostr_db_max_size_gb")]
260 pub db_max_size_gb: u64,
261 #[serde(default = "default_nostr_spambox_max_size_gb")]
264 pub spambox_max_size_gb: u64,
265 #[serde(default)]
267 pub negentropy_only: bool,
268 #[serde(default = "default_nostr_overmute_threshold")]
270 pub overmute_threshold: f64,
271 #[serde(default = "default_nostr_mirror_kinds")]
273 pub mirror_kinds: Vec<u16>,
274 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
276 pub history_sync_author_chunk_size: usize,
277 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
279 pub history_sync_per_author_event_limit: usize,
280 #[serde(default = "default_nostr_history_sync_on_reconnect")]
282 pub history_sync_on_reconnect: bool,
283 #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
286 pub full_text_note_history_follow_distance: Option<u32>,
287 #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
290 pub full_text_note_history_max_relay_pages: usize,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct BlossomConfig {
295 #[serde(default = "default_blossom_enabled")]
296 pub enabled: bool,
297 #[serde(default)]
299 pub servers: Vec<String>,
300 #[serde(default = "default_read_servers")]
302 pub read_servers: Vec<String>,
303 #[serde(default = "default_write_servers")]
305 pub write_servers: Vec<String>,
306 #[serde(default = "default_max_upload_mb")]
308 pub max_upload_mb: u64,
309 #[serde(default = "default_require_random_untrusted_ingest")]
311 pub require_random_untrusted_ingest: bool,
312 #[serde(default = "default_optimistic_uploads")]
315 pub optimistic_uploads: bool,
316}
317
318impl BlossomConfig {
319 pub fn all_read_servers(&self) -> Vec<String> {
320 if !self.enabled {
321 return Vec::new();
322 }
323 let mut servers = self.servers.clone();
324 servers.extend(self.read_servers.clone());
325 servers.extend(self.write_servers.clone());
326 if servers.is_empty() {
327 servers = default_read_servers();
328 servers.extend(default_write_servers());
329 }
330 servers.sort();
331 servers.dedup();
332 servers
333 }
334
335 pub fn all_write_servers(&self) -> Vec<String> {
336 if !self.enabled {
337 return Vec::new();
338 }
339 let mut servers = self.servers.clone();
340 servers.extend(self.write_servers.clone());
341 if servers.is_empty() {
342 servers = default_write_servers();
343 }
344 servers.sort();
345 servers.dedup();
346 servers
347 }
348}
349
350impl NostrConfig {
351 pub fn active_relays(&self) -> Vec<String> {
352 if self.enabled {
353 self.relays.clone()
354 } else {
355 Vec::new()
356 }
357 }
358}
359
360fn default_read_servers() -> Vec<String> {
362 let mut servers = vec![
363 "https://blossom.primal.net".to_string(),
364 "https://cdn.iris.to".to_string(),
365 ];
366 servers.sort();
367 servers
368}
369
370fn default_write_servers() -> Vec<String> {
371 vec!["https://upload.iris.to".to_string()]
372}
373
374fn default_max_upload_mb() -> u64 {
375 5
376}
377
378fn default_require_random_untrusted_ingest() -> bool {
379 true
380}
381
382fn default_optimistic_uploads() -> bool {
383 false
384}
385
386fn default_nostr_enabled() -> bool {
387 true
388}
389
390fn default_blossom_enabled() -> bool {
391 true
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct SyncConfig {
396 #[serde(default = "default_sync_enabled")]
398 pub enabled: bool,
399 #[serde(default = "default_sync_own")]
401 pub sync_own: bool,
402 #[serde(default = "default_sync_followed")]
404 pub sync_followed: bool,
405 #[serde(default = "default_max_concurrent")]
407 pub max_concurrent: usize,
408 #[serde(default = "default_webrtc_timeout_ms")]
410 pub webrtc_timeout_ms: u64,
411 #[serde(default = "default_blossom_timeout_ms")]
413 pub blossom_timeout_ms: u64,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct CashuConfig {
418 #[serde(default)]
420 pub accepted_mints: Vec<String>,
421 #[serde(default)]
423 pub default_mint: Option<String>,
424 #[serde(default = "default_cashu_quote_payment_offer_sat")]
426 pub quote_payment_offer_sat: u64,
427 #[serde(default = "default_cashu_quote_ttl_ms")]
429 pub quote_ttl_ms: u32,
430 #[serde(default = "default_cashu_settlement_timeout_ms")]
432 pub settlement_timeout_ms: u64,
433 #[serde(default = "default_cashu_mint_failure_block_threshold")]
435 pub mint_failure_block_threshold: u64,
436 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
438 pub peer_suggested_mint_base_cap_sat: u64,
439 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
441 pub peer_suggested_mint_success_step_sat: u64,
442 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
444 pub peer_suggested_mint_receipt_step_sat: u64,
445 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
447 pub peer_suggested_mint_max_cap_sat: u64,
448 #[serde(default)]
450 pub payment_default_block_threshold: u64,
451 #[serde(default = "default_cashu_chunk_target_bytes")]
453 pub chunk_target_bytes: usize,
454}
455
456impl Default for CashuConfig {
457 fn default() -> Self {
458 Self {
459 accepted_mints: Vec::new(),
460 default_mint: None,
461 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
462 quote_ttl_ms: default_cashu_quote_ttl_ms(),
463 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
464 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
465 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
466 peer_suggested_mint_success_step_sat:
467 default_cashu_peer_suggested_mint_success_step_sat(),
468 peer_suggested_mint_receipt_step_sat:
469 default_cashu_peer_suggested_mint_receipt_step_sat(),
470 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
471 payment_default_block_threshold: 0,
472 chunk_target_bytes: default_cashu_chunk_target_bytes(),
473 }
474 }
475}
476
477fn default_cashu_quote_payment_offer_sat() -> u64 {
478 3
479}
480
481fn default_cashu_quote_ttl_ms() -> u32 {
482 1_500
483}
484
485fn default_cashu_settlement_timeout_ms() -> u64 {
486 5_000
487}
488
489fn default_cashu_mint_failure_block_threshold() -> u64 {
490 2
491}
492
493fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
494 3
495}
496
497fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
498 1
499}
500
501fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
502 2
503}
504
505fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
506 21
507}
508
509fn default_cashu_chunk_target_bytes() -> usize {
510 32 * 1024
511}
512
513fn default_sync_enabled() -> bool {
514 true
515}
516
517fn default_sync_own() -> bool {
518 true
519}
520
521fn default_sync_followed() -> bool {
522 true
523}
524
525fn default_max_concurrent() -> usize {
526 3
527}
528
529fn default_webrtc_timeout_ms() -> u64 {
530 2000
531}
532
533fn default_blossom_timeout_ms() -> u64 {
534 10000
535}
536
537fn default_social_graph_crawl_depth() -> u32 {
538 2
539}
540
541fn default_nostr_bootstrap_follows() -> Vec<String> {
542 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
543}
544
545fn default_max_write_distance() -> u32 {
546 3
547}
548
549fn default_nostr_db_max_size_gb() -> u64 {
550 10
551}
552
553fn default_nostr_spambox_max_size_gb() -> u64 {
554 1
555}
556
557fn default_nostr_history_sync_on_reconnect() -> bool {
558 true
559}
560
561fn default_nostr_overmute_threshold() -> f64 {
562 1.0
563}
564
565fn default_nostr_mirror_kinds() -> Vec<u16> {
566 vec![0, 1, 3, 6, 7, 9_735, 30_023]
567}
568
569fn default_nostr_history_sync_author_chunk_size() -> usize {
570 5_000
571}
572
573fn default_nostr_history_sync_per_author_event_limit() -> usize {
574 256
575}
576
577fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
578 Some(2)
579}
580
581fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
582 0
583}
584
585fn default_relays() -> Vec<String> {
586 vec![
587 "wss://relay.damus.io".to_string(),
588 "wss://relay.snort.social".to_string(),
589 "wss://temp.iris.to".to_string(),
590 "wss://upload.iris.to/nostr".to_string(),
591 ]
592}
593
594fn default_bind_address() -> String {
595 "127.0.0.1:8080".to_string()
596}
597
598fn default_enable_auth() -> bool {
599 true
600}
601
602fn default_stun_port() -> u16 {
603 3478 }
605
606fn default_enable_webrtc() -> bool {
607 true
608}
609
610fn default_enable_fips() -> bool {
611 true
612}
613
614fn default_fips_discovery_scope() -> String {
615 "hashtree-v1".to_string()
616}
617
618fn default_enable_fips_udp() -> bool {
619 true
620}
621
622fn default_enable_fips_webrtc() -> bool {
623 true
624}
625
626fn default_fetch_from_fips_peers() -> bool {
627 true
628}
629
630fn default_fips_request_timeout_ms() -> u64 {
631 5_500
632}
633
634fn default_http_webrtc_fetch() -> bool {
635 true
636}
637
638fn default_enable_multicast() -> bool {
639 true
640}
641
642fn default_multicast_group() -> String {
643 "239.255.42.98".to_string()
644}
645
646fn default_multicast_port() -> u16 {
647 48555
648}
649
650fn default_max_multicast_peers() -> usize {
651 12
652}
653
654fn default_enable_wifi_aware() -> bool {
655 false
656}
657
658fn default_max_wifi_aware_peers() -> usize {
659 0
660}
661
662fn default_enable_bluetooth() -> bool {
663 false
664}
665
666fn default_max_bluetooth_peers() -> usize {
667 0
668}
669
670fn default_data_dir() -> String {
671 hashtree_config::get_hashtree_dir()
672 .join("data")
673 .to_string_lossy()
674 .to_string()
675}
676
677fn default_max_size_gb() -> u64 {
678 10
679}
680
681fn default_storage_evict_orphans() -> bool {
682 true
683}
684
685impl Default for ServerConfig {
686 fn default() -> Self {
687 Self {
688 mode: ServerMode::default(),
689 bind_address: default_bind_address(),
690 enable_auth: default_enable_auth(),
691 stun_port: default_stun_port(),
692 enable_webrtc: default_enable_webrtc(),
693 enable_fips: default_enable_fips(),
694 fips_discovery_scope: default_fips_discovery_scope(),
695 fips_relays: Vec::new(),
696 enable_fips_udp: default_enable_fips_udp(),
697 fips_udp_bind_addr: None,
698 fips_udp_public: false,
699 fips_udp_external_addr: None,
700 enable_fips_webrtc: default_enable_fips_webrtc(),
701 fetch_from_fips_peers: default_fetch_from_fips_peers(),
702 fips_request_timeout_ms: default_fips_request_timeout_ms(),
703 http_webrtc_fetch: default_http_webrtc_fetch(),
704 peer_signal_urls: Vec::new(),
705 enable_multicast: default_enable_multicast(),
706 multicast_group: default_multicast_group(),
707 multicast_port: default_multicast_port(),
708 max_multicast_peers: default_max_multicast_peers(),
709 enable_wifi_aware: default_enable_wifi_aware(),
710 max_wifi_aware_peers: default_max_wifi_aware_peers(),
711 enable_bluetooth: default_enable_bluetooth(),
712 max_bluetooth_peers: default_max_bluetooth_peers(),
713 public_writes: default_public_writes(),
714 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
715 }
716 }
717}
718
719impl Default for StorageConfig {
720 fn default() -> Self {
721 Self {
722 data_dir: default_data_dir(),
723 max_size_gb: default_max_size_gb(),
724 evict_orphans: default_storage_evict_orphans(),
725 s3: None,
726 }
727 }
728}
729
730impl Default for NostrConfig {
731 fn default() -> Self {
732 Self {
733 enabled: default_nostr_enabled(),
734 relays: default_relays(),
735 allowed_npubs: Vec::new(),
736 socialgraph_root: None,
737 bootstrap_follows: default_nostr_bootstrap_follows(),
738 social_graph_crawl_depth: default_social_graph_crawl_depth(),
739 mirror_max_follow_distance: None,
740 max_write_distance: default_max_write_distance(),
741 db_max_size_gb: default_nostr_db_max_size_gb(),
742 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
743 negentropy_only: false,
744 overmute_threshold: default_nostr_overmute_threshold(),
745 mirror_kinds: default_nostr_mirror_kinds(),
746 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
747 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
748 ),
749 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
750 full_text_note_history_follow_distance:
751 default_nostr_full_text_note_history_follow_distance(),
752 full_text_note_history_max_relay_pages:
753 default_nostr_full_text_note_history_max_relay_pages(),
754 }
755 }
756}
757
758impl Default for BlossomConfig {
759 fn default() -> Self {
760 Self {
761 enabled: default_blossom_enabled(),
762 servers: Vec::new(),
763 read_servers: default_read_servers(),
764 write_servers: default_write_servers(),
765 max_upload_mb: default_max_upload_mb(),
766 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
767 optimistic_uploads: default_optimistic_uploads(),
768 }
769 }
770}
771
772impl Default for SyncConfig {
773 fn default() -> Self {
774 Self {
775 enabled: default_sync_enabled(),
776 sync_own: default_sync_own(),
777 sync_followed: default_sync_followed(),
778 max_concurrent: default_max_concurrent(),
779 webrtc_timeout_ms: default_webrtc_timeout_ms(),
780 blossom_timeout_ms: default_blossom_timeout_ms(),
781 }
782 }
783}
784
785impl Config {
786 pub fn load() -> Result<Self> {
788 let config_path = get_config_path();
789
790 if config_path.exists() {
791 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
792 toml::from_str(&content).context("Failed to parse config file")
793 } else {
794 let config = Config::default();
795 config.save()?;
796 Ok(config)
797 }
798 }
799
800 pub fn save(&self) -> Result<()> {
802 let config_path = get_config_path();
803
804 if let Some(parent) = config_path.parent() {
806 fs::create_dir_all(parent)?;
807 }
808
809 let content = toml::to_string_pretty(self)?;
810 fs::write(&config_path, content)?;
811
812 Ok(())
813 }
814}
815
816pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
818
819fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
820 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
821 let entries = hashtree_config::parse_keys_file(&content);
822 let nsec_str = entries
823 .into_iter()
824 .next()
825 .map(|e| e.secret)
826 .context("Keys file is empty")?;
827 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
828 Ok(Keys::new(secret_key))
829}
830
831fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
832 if let (Some(data_dir), Some(config)) = (data_dir, config) {
833 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
834 }
835}
836
837fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
838 if let Some(parent) = keys_path.parent() {
839 fs::create_dir_all(parent)?;
840 }
841
842 let nsec = keys
843 .secret_key()
844 .to_bech32()
845 .context("Failed to encode nsec")?;
846 fs::write(keys_path, &nsec)?;
847
848 #[cfg(unix)]
849 {
850 use std::os::unix::fs::PermissionsExt;
851 let perms = fs::Permissions::from_mode(0o600);
852 fs::set_permissions(keys_path, perms)?;
853 }
854
855 Ok(())
856}
857
858pub fn ensure_auth_cookie() -> Result<(String, String)> {
860 let cookie_path = get_auth_cookie_path();
861
862 if cookie_path.exists() {
863 read_auth_cookie()
864 } else {
865 generate_auth_cookie()
866 }
867}
868
869pub fn read_auth_cookie() -> Result<(String, String)> {
871 let cookie_path = get_auth_cookie_path();
872 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
873
874 let parts: Vec<&str> = content.trim().split(':').collect();
875 if parts.len() != 2 {
876 anyhow::bail!("Invalid auth cookie format");
877 }
878
879 Ok((parts[0].to_string(), parts[1].to_string()))
880}
881
882pub fn ensure_keys() -> Result<(Keys, bool)> {
885 let config_dir = get_hashtree_dir();
886 let config = Config::load().ok();
887 let data_dir = config
888 .as_ref()
889 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
890 ensure_keys_in(&config_dir, data_dir, config.as_ref())
891}
892
893pub fn ensure_keys_in(
896 config_dir: &Path,
897 data_dir: Option<&Path>,
898 config: Option<&Config>,
899) -> Result<(Keys, bool)> {
900 let keys_path = config_dir.join("keys");
901
902 if keys_path.exists() {
903 Ok((read_keys_from_path(&keys_path)?, false))
904 } else {
905 let keys = generate_keys_in(config_dir, data_dir, config)?;
906 Ok((keys, true))
907 }
908}
909
910pub fn read_keys() -> Result<Keys> {
912 read_keys_in(&get_hashtree_dir())
913}
914
915pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
917 read_keys_from_path(&config_dir.join("keys"))
918}
919
920pub fn ensure_keys_string() -> Result<(String, bool)> {
923 let config_dir = get_hashtree_dir();
924 let config = Config::load().ok();
925 let data_dir = config
926 .as_ref()
927 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
928 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
929}
930
931pub fn ensure_keys_string_in(
934 config_dir: &Path,
935 data_dir: Option<&Path>,
936 config: Option<&Config>,
937) -> Result<(String, bool)> {
938 let keys_path = config_dir.join("keys");
939
940 if keys_path.exists() {
941 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
942 let entries = hashtree_config::parse_keys_file(&content);
943 let nsec_str = entries
944 .into_iter()
945 .next()
946 .map(|e| e.secret)
947 .context("Keys file is empty")?;
948 Ok((nsec_str, false))
949 } else {
950 let keys = generate_keys_in(config_dir, data_dir, config)?;
951 let nsec = keys
952 .secret_key()
953 .to_bech32()
954 .context("Failed to encode nsec")?;
955 Ok((nsec, true))
956 }
957}
958
959pub fn generate_keys() -> Result<Keys> {
961 let config_dir = get_hashtree_dir();
962 let config = Config::load().ok();
963 let data_dir = config
964 .as_ref()
965 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
966 generate_keys_in(&config_dir, data_dir, config.as_ref())
967}
968
969pub fn generate_keys_in(
972 config_dir: &Path,
973 data_dir: Option<&Path>,
974 config: Option<&Config>,
975) -> Result<Keys> {
976 let keys = Keys::generate();
977 write_keys_to_path(&config_dir.join("keys"), &keys)?;
978 seed_identity_defaults_if_needed(data_dir, config);
979 Ok(keys)
980}
981
982pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
984 keys.public_key().to_bytes()
985}
986
987pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
989 use nostr::PublicKey;
990 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
991 Ok(pk.to_bytes())
992}
993
994pub fn generate_auth_cookie() -> Result<(String, String)> {
996 use rand::Rng;
997
998 let cookie_path = get_auth_cookie_path();
999
1000 if let Some(parent) = cookie_path.parent() {
1002 fs::create_dir_all(parent)?;
1003 }
1004
1005 let mut rng = rand::thread_rng();
1007 let username = format!("htree_{}", rng.gen::<u32>());
1008 let password: String = (0..32)
1009 .map(|_| {
1010 let idx = rng.gen_range(0..62);
1011 match idx {
1012 0..=25 => (b'a' + idx) as char,
1013 26..=51 => (b'A' + (idx - 26)) as char,
1014 _ => (b'0' + (idx - 52)) as char,
1015 }
1016 })
1017 .collect();
1018
1019 let content = format!("{}:{}", username, password);
1021 fs::write(&cookie_path, content)?;
1022
1023 #[cfg(unix)]
1025 {
1026 use std::os::unix::fs::PermissionsExt;
1027 let perms = fs::Permissions::from_mode(0o600);
1028 fs::set_permissions(&cookie_path, perms)?;
1029 }
1030
1031 Ok((username, password))
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036 use super::*;
1037 use crate::test_support::{test_env_lock, EnvVarGuard};
1038 use tempfile::TempDir;
1039
1040 #[test]
1041 fn test_config_default() {
1042 let config = Config::default();
1043 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1044 assert!(config.server.enable_auth);
1045 assert!(config.server.enable_multicast);
1046 assert_eq!(config.server.multicast_group, "239.255.42.98");
1047 assert_eq!(config.server.multicast_port, 48555);
1048 assert_eq!(config.server.max_multicast_peers, 12);
1049 assert!(!config.server.enable_wifi_aware);
1050 assert_eq!(config.server.max_wifi_aware_peers, 0);
1051 assert!(!config.server.enable_bluetooth);
1052 assert_eq!(config.server.max_bluetooth_peers, 0);
1053 assert_eq!(config.storage.max_size_gb, 10);
1054 assert!(config.storage.evict_orphans);
1055 assert!(config.nostr.enabled);
1056 assert!(config
1057 .nostr
1058 .relays
1059 .contains(&"wss://upload.iris.to/nostr".to_string()));
1060 assert!(config.blossom.enabled);
1061 assert!(!config.blossom.optimistic_uploads);
1062 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1063 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1064 assert_eq!(config.nostr.max_write_distance, 3);
1065 assert_eq!(config.nostr.db_max_size_gb, 10);
1066 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1067 assert!(!config.nostr.negentropy_only);
1068 assert_eq!(config.nostr.overmute_threshold, 1.0);
1069 assert_eq!(
1070 config.nostr.mirror_kinds,
1071 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1072 );
1073 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1074 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1075 assert!(config.nostr.history_sync_on_reconnect);
1076 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1077 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1078 assert!(config.nostr.socialgraph_root.is_none());
1079 assert_eq!(
1080 config.nostr.bootstrap_follows,
1081 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1082 );
1083 assert!(!config.server.socialgraph_snapshot_public);
1084 assert!(config.cashu.accepted_mints.is_empty());
1085 assert!(config.cashu.default_mint.is_none());
1086 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1087 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1088 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1089 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1090 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1091 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1092 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1093 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1094 assert_eq!(config.cashu.payment_default_block_threshold, 0);
1095 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1096 }
1097
1098 #[test]
1099 fn test_blossom_optimistic_uploads_deserialize() {
1100 let toml_str = r#"
1101[blossom]
1102optimistic_uploads = true
1103"#;
1104 let config: Config = toml::from_str(toml_str).unwrap();
1105 assert!(config.blossom.optimistic_uploads);
1106 assert!(config.blossom.require_random_untrusted_ingest);
1107 }
1108
1109 #[test]
1110 fn test_nostr_config_deserialize_with_defaults() {
1111 let toml_str = r#"
1112[nostr]
1113relays = ["wss://relay.damus.io"]
1114"#;
1115 let config: Config = toml::from_str(toml_str).unwrap();
1116 assert!(config.nostr.enabled);
1117 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1118 assert!(config.storage.evict_orphans);
1119 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1120 assert_eq!(config.nostr.mirror_max_follow_distance, None);
1121 assert_eq!(config.nostr.max_write_distance, 3);
1122 assert_eq!(config.nostr.db_max_size_gb, 10);
1123 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1124 assert!(!config.nostr.negentropy_only);
1125 assert_eq!(config.nostr.overmute_threshold, 1.0);
1126 assert_eq!(
1127 config.nostr.mirror_kinds,
1128 vec![0, 1, 3, 6, 7, 9_735, 30_023]
1129 );
1130 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1131 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1132 assert!(config.nostr.history_sync_on_reconnect);
1133 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1134 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1135 assert!(config.nostr.socialgraph_root.is_none());
1136 assert_eq!(
1137 config.nostr.bootstrap_follows,
1138 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1139 );
1140 }
1141
1142 #[test]
1143 fn test_nostr_config_deserialize_with_socialgraph() {
1144 let toml_str = r#"
1145[nostr]
1146relays = ["wss://relay.damus.io"]
1147socialgraph_root = "npub1test"
1148bootstrap_follows = []
1149social_graph_crawl_depth = 3
1150mirror_max_follow_distance = 2
1151max_write_distance = 5
1152negentropy_only = true
1153overmute_threshold = 2.5
1154mirror_kinds = [0, 10000]
1155history_sync_author_chunk_size = 250
1156history_sync_per_author_event_limit = 128
1157history_sync_on_reconnect = false
1158full_text_note_history_follow_distance = 1
1159full_text_note_history_max_relay_pages = 64
1160"#;
1161 let config: Config = toml::from_str(toml_str).unwrap();
1162 assert!(config.nostr.enabled);
1163 assert!(config.storage.evict_orphans);
1164 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1165 assert!(config.nostr.bootstrap_follows.is_empty());
1166 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1167 assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1168 assert_eq!(config.nostr.max_write_distance, 5);
1169 assert_eq!(config.nostr.db_max_size_gb, 10);
1170 assert_eq!(config.nostr.spambox_max_size_gb, 1);
1171 assert!(config.nostr.negentropy_only);
1172 assert_eq!(config.nostr.overmute_threshold, 2.5);
1173 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1174 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1175 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1176 assert!(!config.nostr.history_sync_on_reconnect);
1177 assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1178 assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1179 }
1180
1181 #[test]
1182 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1183 let toml_str = r#"
1184[nostr]
1185relays = ["wss://relay.damus.io"]
1186crawl_depth = 4
1187"#;
1188 let config: Config = toml::from_str(toml_str).unwrap();
1189 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1190 }
1191
1192 #[test]
1193 fn test_storage_config_disables_orphan_eviction_when_requested() {
1194 let toml_str = r#"
1195[storage]
1196evict_orphans = false
1197"#;
1198 let config: Config = toml::from_str(toml_str).unwrap();
1199 assert!(!config.storage.evict_orphans);
1200 }
1201
1202 #[test]
1203 fn test_server_config_deserialize_with_multicast() {
1204 let toml_str = r#"
1205[server]
1206enable_multicast = true
1207multicast_group = "239.255.42.99"
1208multicast_port = 49001
1209max_multicast_peers = 12
1210enable_wifi_aware = true
1211max_wifi_aware_peers = 5
1212enable_bluetooth = true
1213max_bluetooth_peers = 6
1214"#;
1215 let config: Config = toml::from_str(toml_str).unwrap();
1216 assert!(config.server.enable_multicast);
1217 assert_eq!(config.server.multicast_group, "239.255.42.99");
1218 assert_eq!(config.server.multicast_port, 49_001);
1219 assert_eq!(config.server.max_multicast_peers, 12);
1220 assert!(config.server.enable_wifi_aware);
1221 assert_eq!(config.server.max_wifi_aware_peers, 5);
1222 assert!(config.server.enable_bluetooth);
1223 assert_eq!(config.server.max_bluetooth_peers, 6);
1224 }
1225
1226 #[test]
1227 fn test_cashu_config_deserialize_with_accepted_mints() {
1228 let toml_str = r#"
1229[cashu]
1230accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1231default_mint = "https://mint1.example"
1232quote_payment_offer_sat = 5
1233quote_ttl_ms = 2500
1234settlement_timeout_ms = 7000
1235mint_failure_block_threshold = 3
1236peer_suggested_mint_base_cap_sat = 4
1237peer_suggested_mint_success_step_sat = 2
1238peer_suggested_mint_receipt_step_sat = 3
1239peer_suggested_mint_max_cap_sat = 34
1240payment_default_block_threshold = 2
1241chunk_target_bytes = 65536
1242"#;
1243 let config: Config = toml::from_str(toml_str).unwrap();
1244 assert_eq!(
1245 config.cashu.accepted_mints,
1246 vec![
1247 "https://mint1.example".to_string(),
1248 "http://127.0.0.1:3338".to_string()
1249 ]
1250 );
1251 assert_eq!(
1252 config.cashu.default_mint,
1253 Some("https://mint1.example".to_string())
1254 );
1255 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1256 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1257 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1258 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1259 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1260 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1261 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1262 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1263 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1264 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1265 }
1266
1267 #[test]
1268 fn test_auth_cookie_generation() -> Result<()> {
1269 let _lock = test_env_lock()
1270 .lock()
1271 .unwrap_or_else(|err| err.into_inner());
1272 let temp_dir = TempDir::new()?;
1273 let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1274
1275 let (username, password) = generate_auth_cookie()?;
1276
1277 assert!(username.starts_with("htree_"));
1278 assert_eq!(password.len(), 32);
1279
1280 let cookie_path = get_auth_cookie_path();
1282 assert!(cookie_path.exists());
1283
1284 let (u2, p2) = read_auth_cookie()?;
1286 assert_eq!(username, u2);
1287 assert_eq!(password, p2);
1288
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1294 let mut config = BlossomConfig::default();
1295 config.servers = vec!["https://legacy.server".to_string()];
1296
1297 let read = config.all_read_servers();
1298 assert!(read.contains(&"https://legacy.server".to_string()));
1299 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1300 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1301 assert!(read.contains(&"https://upload.iris.to".to_string()));
1302
1303 let write = config.all_write_servers();
1304 assert!(write.contains(&"https://legacy.server".to_string()));
1305 assert!(write.contains(&"https://upload.iris.to".to_string()));
1306 }
1307
1308 #[test]
1309 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1310 let config = BlossomConfig {
1311 enabled: true,
1312 servers: Vec::new(),
1313 read_servers: Vec::new(),
1314 write_servers: Vec::new(),
1315 max_upload_mb: default_max_upload_mb(),
1316 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1317 optimistic_uploads: default_optimistic_uploads(),
1318 };
1319
1320 let read = config.all_read_servers();
1321 let mut expected = default_read_servers();
1322 expected.extend(default_write_servers());
1323 expected.sort();
1324 expected.dedup();
1325 assert_eq!(read, expected);
1326
1327 let write = config.all_write_servers();
1328 assert_eq!(write, default_write_servers());
1329 }
1330
1331 #[test]
1332 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1333 let nostr = NostrConfig {
1334 enabled: false,
1335 relays: vec!["wss://relay.example".to_string()],
1336 ..NostrConfig::default()
1337 };
1338 assert!(nostr.active_relays().is_empty());
1339
1340 let blossom = BlossomConfig {
1341 enabled: false,
1342 servers: vec!["https://legacy.server".to_string()],
1343 read_servers: vec!["https://read.example".to_string()],
1344 write_servers: vec!["https://write.example".to_string()],
1345 max_upload_mb: default_max_upload_mb(),
1346 require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1347 optimistic_uploads: default_optimistic_uploads(),
1348 };
1349 assert!(blossom.all_read_servers().is_empty());
1350 assert!(blossom.all_write_servers().is_empty());
1351 }
1352
1353 #[test]
1354 fn server_defaults_enable_fips_udp_and_webrtc() {
1355 let server = ServerConfig::default();
1356
1357 assert!(server.enable_fips);
1358 assert!(server.enable_fips_udp);
1359 assert!(server.fips_udp_bind_addr.is_none());
1360 assert!(!server.fips_udp_public);
1361 assert!(server.fips_udp_external_addr.is_none());
1362 assert!(server.enable_fips_webrtc);
1363 assert!(server.fetch_from_fips_peers);
1364 assert!(server.fips_relays.is_empty());
1365 assert_eq!(server.fips_discovery_scope, "hashtree-v1");
1366 assert_eq!(server.fips_request_timeout_ms, 5_500);
1367 }
1368
1369 #[test]
1370 fn server_config_reads_fips_overrides() {
1371 let config: Config = toml::from_str(
1372 r#"
1373[server]
1374enable_fips = true
1375fips_discovery_scope = "test-hashtree"
1376fips_relays = ["wss://fips.example"]
1377enable_fips_udp = false
1378fips_udp_bind_addr = "0.0.0.0:2121"
1379fips_udp_public = true
1380fips_udp_external_addr = "198.19.77.10:2121"
1381enable_fips_webrtc = true
1382fetch_from_fips_peers = false
1383fips_request_timeout_ms = 42
1384"#,
1385 )
1386 .unwrap();
1387
1388 assert!(config.server.enable_fips);
1389 assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1390 assert_eq!(config.server.fips_relays, ["wss://fips.example"]);
1391 assert!(!config.server.enable_fips_udp);
1392 assert_eq!(
1393 config.server.fips_udp_bind_addr.as_deref(),
1394 Some("0.0.0.0:2121")
1395 );
1396 assert!(config.server.fips_udp_public);
1397 assert_eq!(
1398 config.server.fips_udp_external_addr.as_deref(),
1399 Some("198.19.77.10:2121")
1400 );
1401 assert!(config.server.enable_fips_webrtc);
1402 assert!(!config.server.fetch_from_fips_peers);
1403 assert_eq!(config.server.fips_request_timeout_ms, 42);
1404 }
1405
1406 #[test]
1407 fn server_config_accepts_legacy_http_fips_fetch_name() {
1408 let config: Config = toml::from_str(
1409 r#"
1410[server]
1411http_fips_fetch = false
1412"#,
1413 )
1414 .unwrap();
1415
1416 assert!(!config.server.fetch_from_fips_peers);
1417 }
1418
1419 #[test]
1420 fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1421 let active_nostr = vec!["wss://nostr.example".to_string()];
1422 let mut server = ServerConfig::default();
1423
1424 assert_eq!(server.resolved_fips_relays(&active_nostr), active_nostr);
1425
1426 server.fips_relays = vec!["wss://fips.example".to_string()];
1427 assert_eq!(
1428 server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1429 ["wss://fips.example"]
1430 );
1431 }
1432}