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}
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "kebab-case")]
26pub enum ServerMode {
27 #[default]
28 Normal,
29 #[serde(alias = "signal-only")]
30 Assist,
31}
32
33impl ServerMode {
34 pub const fn as_str(self) -> &'static str {
35 match self {
36 Self::Normal => "normal",
37 Self::Assist => "assist",
38 }
39 }
40
41 pub const fn hash_get_enabled(self) -> bool {
42 matches!(self, Self::Normal)
43 }
44
45 pub const fn background_services_enabled(self) -> bool {
46 matches!(self, Self::Normal)
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ServerConfig {
52 #[serde(default)]
53 pub mode: ServerMode,
54 #[serde(default = "default_bind_address")]
55 pub bind_address: String,
56 #[serde(default = "default_enable_auth")]
57 pub enable_auth: bool,
58 #[serde(default = "default_stun_port")]
60 pub stun_port: u16,
61 #[serde(default = "default_enable_webrtc")]
63 pub enable_webrtc: bool,
64 #[serde(default = "default_enable_multicast")]
66 pub enable_multicast: bool,
67 #[serde(default = "default_multicast_group")]
69 pub multicast_group: String,
70 #[serde(default = "default_multicast_port")]
72 pub multicast_port: u16,
73 #[serde(default = "default_max_multicast_peers")]
76 pub max_multicast_peers: usize,
77 #[serde(default = "default_enable_wifi_aware")]
79 pub enable_wifi_aware: bool,
80 #[serde(default = "default_max_wifi_aware_peers")]
83 pub max_wifi_aware_peers: usize,
84 #[serde(default = "default_enable_bluetooth")]
86 pub enable_bluetooth: bool,
87 #[serde(default = "default_max_bluetooth_peers")]
90 pub max_bluetooth_peers: usize,
91 #[serde(default = "default_public_writes")]
94 pub public_writes: bool,
95 #[serde(default = "default_socialgraph_snapshot_public")]
97 pub socialgraph_snapshot_public: bool,
98}
99
100fn default_public_writes() -> bool {
101 true
102}
103
104fn default_socialgraph_snapshot_public() -> bool {
105 false
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct StorageConfig {
110 #[serde(default = "default_data_dir")]
111 pub data_dir: String,
112 #[serde(default = "default_max_size_gb")]
113 pub max_size_gb: u64,
114 #[serde(default = "default_storage_evict_orphans")]
115 pub evict_orphans: bool,
116 #[serde(default)]
118 pub s3: Option<S3Config>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct S3Config {
124 pub endpoint: String,
126 pub bucket: String,
128 #[serde(default)]
130 pub prefix: Option<String>,
131 #[serde(default = "default_s3_region")]
133 pub region: String,
134 #[serde(default)]
136 pub access_key: Option<String>,
137 #[serde(default)]
139 pub secret_key: Option<String>,
140 #[serde(default)]
142 pub public_url: Option<String>,
143}
144
145fn default_s3_region() -> String {
146 "auto".to_string()
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct NostrConfig {
151 #[serde(default = "default_nostr_enabled")]
152 pub enabled: bool,
153 #[serde(default = "default_relays")]
154 pub relays: Vec<String>,
155 #[serde(default)]
157 pub allowed_npubs: Vec<String>,
158 #[serde(default)]
160 pub socialgraph_root: Option<String>,
161 #[serde(default = "default_nostr_bootstrap_follows")]
164 pub bootstrap_follows: Vec<String>,
165 #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
167 pub social_graph_crawl_depth: u32,
168 #[serde(default = "default_max_write_distance")]
170 pub max_write_distance: u32,
171 #[serde(default = "default_nostr_db_max_size_gb")]
173 pub db_max_size_gb: u64,
174 #[serde(default = "default_nostr_spambox_max_size_gb")]
177 pub spambox_max_size_gb: u64,
178 #[serde(default)]
180 pub negentropy_only: bool,
181 #[serde(default = "default_nostr_overmute_threshold")]
183 pub overmute_threshold: f64,
184 #[serde(default = "default_nostr_mirror_kinds")]
186 pub mirror_kinds: Vec<u16>,
187 #[serde(default = "default_nostr_history_sync_author_chunk_size")]
189 pub history_sync_author_chunk_size: usize,
190 #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
192 pub history_sync_per_author_event_limit: usize,
193 #[serde(default = "default_nostr_history_sync_on_reconnect")]
195 pub history_sync_on_reconnect: bool,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BlossomConfig {
200 #[serde(default = "default_blossom_enabled")]
201 pub enabled: bool,
202 #[serde(default)]
204 pub servers: Vec<String>,
205 #[serde(default = "default_read_servers")]
207 pub read_servers: Vec<String>,
208 #[serde(default = "default_write_servers")]
210 pub write_servers: Vec<String>,
211 #[serde(default = "default_max_upload_mb")]
213 pub max_upload_mb: u64,
214}
215
216impl BlossomConfig {
217 pub fn all_read_servers(&self) -> Vec<String> {
218 if !self.enabled {
219 return Vec::new();
220 }
221 let mut servers = self.servers.clone();
222 servers.extend(self.read_servers.clone());
223 servers.extend(self.write_servers.clone());
224 if servers.is_empty() {
225 servers = default_read_servers();
226 servers.extend(default_write_servers());
227 }
228 servers.sort();
229 servers.dedup();
230 servers
231 }
232
233 pub fn all_write_servers(&self) -> Vec<String> {
234 if !self.enabled {
235 return Vec::new();
236 }
237 let mut servers = self.servers.clone();
238 servers.extend(self.write_servers.clone());
239 if servers.is_empty() {
240 servers = default_write_servers();
241 }
242 servers.sort();
243 servers.dedup();
244 servers
245 }
246}
247
248impl NostrConfig {
249 pub fn active_relays(&self) -> Vec<String> {
250 if self.enabled {
251 self.relays.clone()
252 } else {
253 Vec::new()
254 }
255 }
256}
257
258fn default_read_servers() -> Vec<String> {
260 let mut servers = vec![
261 "https://blossom.primal.net".to_string(),
262 "https://cdn.iris.to".to_string(),
263 "https://hashtree.iris.to".to_string(),
264 ];
265 servers.sort();
266 servers
267}
268
269fn default_write_servers() -> Vec<String> {
270 vec!["https://upload.iris.to".to_string()]
271}
272
273fn default_max_upload_mb() -> u64 {
274 5
275}
276
277fn default_nostr_enabled() -> bool {
278 true
279}
280
281fn default_blossom_enabled() -> bool {
282 true
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct SyncConfig {
287 #[serde(default = "default_sync_enabled")]
289 pub enabled: bool,
290 #[serde(default = "default_sync_own")]
292 pub sync_own: bool,
293 #[serde(default = "default_sync_followed")]
295 pub sync_followed: bool,
296 #[serde(default = "default_max_concurrent")]
298 pub max_concurrent: usize,
299 #[serde(default = "default_webrtc_timeout_ms")]
301 pub webrtc_timeout_ms: u64,
302 #[serde(default = "default_blossom_timeout_ms")]
304 pub blossom_timeout_ms: u64,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct CashuConfig {
309 #[serde(default)]
311 pub accepted_mints: Vec<String>,
312 #[serde(default)]
314 pub default_mint: Option<String>,
315 #[serde(default = "default_cashu_quote_payment_offer_sat")]
317 pub quote_payment_offer_sat: u64,
318 #[serde(default = "default_cashu_quote_ttl_ms")]
320 pub quote_ttl_ms: u32,
321 #[serde(default = "default_cashu_settlement_timeout_ms")]
323 pub settlement_timeout_ms: u64,
324 #[serde(default = "default_cashu_mint_failure_block_threshold")]
326 pub mint_failure_block_threshold: u64,
327 #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
329 pub peer_suggested_mint_base_cap_sat: u64,
330 #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
332 pub peer_suggested_mint_success_step_sat: u64,
333 #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
335 pub peer_suggested_mint_receipt_step_sat: u64,
336 #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
338 pub peer_suggested_mint_max_cap_sat: u64,
339 #[serde(default)]
341 pub payment_default_block_threshold: u64,
342 #[serde(default = "default_cashu_chunk_target_bytes")]
344 pub chunk_target_bytes: usize,
345}
346
347impl Default for CashuConfig {
348 fn default() -> Self {
349 Self {
350 accepted_mints: Vec::new(),
351 default_mint: None,
352 quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
353 quote_ttl_ms: default_cashu_quote_ttl_ms(),
354 settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
355 mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
356 peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
357 peer_suggested_mint_success_step_sat:
358 default_cashu_peer_suggested_mint_success_step_sat(),
359 peer_suggested_mint_receipt_step_sat:
360 default_cashu_peer_suggested_mint_receipt_step_sat(),
361 peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
362 payment_default_block_threshold: 0,
363 chunk_target_bytes: default_cashu_chunk_target_bytes(),
364 }
365 }
366}
367
368fn default_cashu_quote_payment_offer_sat() -> u64 {
369 3
370}
371
372fn default_cashu_quote_ttl_ms() -> u32 {
373 1_500
374}
375
376fn default_cashu_settlement_timeout_ms() -> u64 {
377 5_000
378}
379
380fn default_cashu_mint_failure_block_threshold() -> u64 {
381 2
382}
383
384fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
385 3
386}
387
388fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
389 1
390}
391
392fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
393 2
394}
395
396fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
397 21
398}
399
400fn default_cashu_chunk_target_bytes() -> usize {
401 32 * 1024
402}
403
404fn default_sync_enabled() -> bool {
405 true
406}
407
408fn default_sync_own() -> bool {
409 true
410}
411
412fn default_sync_followed() -> bool {
413 true
414}
415
416fn default_max_concurrent() -> usize {
417 3
418}
419
420fn default_webrtc_timeout_ms() -> u64 {
421 2000
422}
423
424fn default_blossom_timeout_ms() -> u64 {
425 10000
426}
427
428fn default_social_graph_crawl_depth() -> u32 {
429 2
430}
431
432fn default_nostr_bootstrap_follows() -> Vec<String> {
433 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
434}
435
436fn default_max_write_distance() -> u32 {
437 3
438}
439
440fn default_nostr_db_max_size_gb() -> u64 {
441 10
442}
443
444fn default_nostr_spambox_max_size_gb() -> u64 {
445 1
446}
447
448fn default_nostr_history_sync_on_reconnect() -> bool {
449 true
450}
451
452fn default_nostr_overmute_threshold() -> f64 {
453 1.0
454}
455
456fn default_nostr_mirror_kinds() -> Vec<u16> {
457 vec![0, 1, 3, 6, 7, 9_735]
458}
459
460fn default_nostr_history_sync_author_chunk_size() -> usize {
461 5_000
462}
463
464fn default_nostr_history_sync_per_author_event_limit() -> usize {
465 256
466}
467
468fn default_relays() -> Vec<String> {
469 vec![
470 "wss://relay.damus.io".to_string(),
471 "wss://relay.snort.social".to_string(),
472 "wss://temp.iris.to".to_string(),
473 "wss://upload.iris.to/nostr".to_string(),
474 ]
475}
476
477fn default_bind_address() -> String {
478 "127.0.0.1:8080".to_string()
479}
480
481fn default_enable_auth() -> bool {
482 true
483}
484
485fn default_stun_port() -> u16 {
486 3478 }
488
489fn default_enable_webrtc() -> bool {
490 true
491}
492
493fn default_enable_multicast() -> bool {
494 true
495}
496
497fn default_multicast_group() -> String {
498 "239.255.42.98".to_string()
499}
500
501fn default_multicast_port() -> u16 {
502 48555
503}
504
505fn default_max_multicast_peers() -> usize {
506 12
507}
508
509fn default_enable_wifi_aware() -> bool {
510 false
511}
512
513fn default_max_wifi_aware_peers() -> usize {
514 0
515}
516
517fn default_enable_bluetooth() -> bool {
518 false
519}
520
521fn default_max_bluetooth_peers() -> usize {
522 0
523}
524
525fn default_data_dir() -> String {
526 hashtree_config::get_hashtree_dir()
527 .join("data")
528 .to_string_lossy()
529 .to_string()
530}
531
532fn default_max_size_gb() -> u64 {
533 10
534}
535
536fn default_storage_evict_orphans() -> bool {
537 true
538}
539
540impl Default for ServerConfig {
541 fn default() -> Self {
542 Self {
543 mode: ServerMode::default(),
544 bind_address: default_bind_address(),
545 enable_auth: default_enable_auth(),
546 stun_port: default_stun_port(),
547 enable_webrtc: default_enable_webrtc(),
548 enable_multicast: default_enable_multicast(),
549 multicast_group: default_multicast_group(),
550 multicast_port: default_multicast_port(),
551 max_multicast_peers: default_max_multicast_peers(),
552 enable_wifi_aware: default_enable_wifi_aware(),
553 max_wifi_aware_peers: default_max_wifi_aware_peers(),
554 enable_bluetooth: default_enable_bluetooth(),
555 max_bluetooth_peers: default_max_bluetooth_peers(),
556 public_writes: default_public_writes(),
557 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
558 }
559 }
560}
561
562impl Default for StorageConfig {
563 fn default() -> Self {
564 Self {
565 data_dir: default_data_dir(),
566 max_size_gb: default_max_size_gb(),
567 evict_orphans: default_storage_evict_orphans(),
568 s3: None,
569 }
570 }
571}
572
573impl Default for NostrConfig {
574 fn default() -> Self {
575 Self {
576 enabled: default_nostr_enabled(),
577 relays: default_relays(),
578 allowed_npubs: Vec::new(),
579 socialgraph_root: None,
580 bootstrap_follows: default_nostr_bootstrap_follows(),
581 social_graph_crawl_depth: default_social_graph_crawl_depth(),
582 max_write_distance: default_max_write_distance(),
583 db_max_size_gb: default_nostr_db_max_size_gb(),
584 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
585 negentropy_only: false,
586 overmute_threshold: default_nostr_overmute_threshold(),
587 mirror_kinds: default_nostr_mirror_kinds(),
588 history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
589 history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
590 ),
591 history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
592 }
593 }
594}
595
596impl Default for BlossomConfig {
597 fn default() -> Self {
598 Self {
599 enabled: default_blossom_enabled(),
600 servers: Vec::new(),
601 read_servers: default_read_servers(),
602 write_servers: default_write_servers(),
603 max_upload_mb: default_max_upload_mb(),
604 }
605 }
606}
607
608impl Default for SyncConfig {
609 fn default() -> Self {
610 Self {
611 enabled: default_sync_enabled(),
612 sync_own: default_sync_own(),
613 sync_followed: default_sync_followed(),
614 max_concurrent: default_max_concurrent(),
615 webrtc_timeout_ms: default_webrtc_timeout_ms(),
616 blossom_timeout_ms: default_blossom_timeout_ms(),
617 }
618 }
619}
620
621impl Config {
622 pub fn load() -> Result<Self> {
624 let config_path = get_config_path();
625
626 if config_path.exists() {
627 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
628 toml::from_str(&content).context("Failed to parse config file")
629 } else {
630 let config = Config::default();
631 config.save()?;
632 Ok(config)
633 }
634 }
635
636 pub fn save(&self) -> Result<()> {
638 let config_path = get_config_path();
639
640 if let Some(parent) = config_path.parent() {
642 fs::create_dir_all(parent)?;
643 }
644
645 let content = toml::to_string_pretty(self)?;
646 fs::write(&config_path, content)?;
647
648 Ok(())
649 }
650}
651
652pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
654
655fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
656 let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
657 let entries = hashtree_config::parse_keys_file(&content);
658 let nsec_str = entries
659 .into_iter()
660 .next()
661 .map(|e| e.secret)
662 .context("Keys file is empty")?;
663 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
664 Ok(Keys::new(secret_key))
665}
666
667fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
668 if let (Some(data_dir), Some(config)) = (data_dir, config) {
669 let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
670 }
671}
672
673fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
674 if let Some(parent) = keys_path.parent() {
675 fs::create_dir_all(parent)?;
676 }
677
678 let nsec = keys
679 .secret_key()
680 .to_bech32()
681 .context("Failed to encode nsec")?;
682 fs::write(keys_path, &nsec)?;
683
684 #[cfg(unix)]
685 {
686 use std::os::unix::fs::PermissionsExt;
687 let perms = fs::Permissions::from_mode(0o600);
688 fs::set_permissions(keys_path, perms)?;
689 }
690
691 Ok(())
692}
693
694pub fn ensure_auth_cookie() -> Result<(String, String)> {
696 let cookie_path = get_auth_cookie_path();
697
698 if cookie_path.exists() {
699 read_auth_cookie()
700 } else {
701 generate_auth_cookie()
702 }
703}
704
705pub fn read_auth_cookie() -> Result<(String, String)> {
707 let cookie_path = get_auth_cookie_path();
708 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
709
710 let parts: Vec<&str> = content.trim().split(':').collect();
711 if parts.len() != 2 {
712 anyhow::bail!("Invalid auth cookie format");
713 }
714
715 Ok((parts[0].to_string(), parts[1].to_string()))
716}
717
718pub fn ensure_keys() -> Result<(Keys, bool)> {
721 let config_dir = get_hashtree_dir();
722 let config = Config::load().ok();
723 let data_dir = config
724 .as_ref()
725 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
726 ensure_keys_in(&config_dir, data_dir, config.as_ref())
727}
728
729pub fn ensure_keys_in(
732 config_dir: &Path,
733 data_dir: Option<&Path>,
734 config: Option<&Config>,
735) -> Result<(Keys, bool)> {
736 let keys_path = config_dir.join("keys");
737
738 if keys_path.exists() {
739 Ok((read_keys_from_path(&keys_path)?, false))
740 } else {
741 let keys = generate_keys_in(config_dir, data_dir, config)?;
742 Ok((keys, true))
743 }
744}
745
746pub fn read_keys() -> Result<Keys> {
748 read_keys_in(&get_hashtree_dir())
749}
750
751pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
753 read_keys_from_path(&config_dir.join("keys"))
754}
755
756pub fn ensure_keys_string() -> Result<(String, bool)> {
759 let config_dir = get_hashtree_dir();
760 let config = Config::load().ok();
761 let data_dir = config
762 .as_ref()
763 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
764 ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
765}
766
767pub fn ensure_keys_string_in(
770 config_dir: &Path,
771 data_dir: Option<&Path>,
772 config: Option<&Config>,
773) -> Result<(String, bool)> {
774 let keys_path = config_dir.join("keys");
775
776 if keys_path.exists() {
777 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
778 let entries = hashtree_config::parse_keys_file(&content);
779 let nsec_str = entries
780 .into_iter()
781 .next()
782 .map(|e| e.secret)
783 .context("Keys file is empty")?;
784 Ok((nsec_str, false))
785 } else {
786 let keys = generate_keys_in(config_dir, data_dir, config)?;
787 let nsec = keys
788 .secret_key()
789 .to_bech32()
790 .context("Failed to encode nsec")?;
791 Ok((nsec, true))
792 }
793}
794
795pub fn generate_keys() -> Result<Keys> {
797 let config_dir = get_hashtree_dir();
798 let config = Config::load().ok();
799 let data_dir = config
800 .as_ref()
801 .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
802 generate_keys_in(&config_dir, data_dir, config.as_ref())
803}
804
805pub fn generate_keys_in(
808 config_dir: &Path,
809 data_dir: Option<&Path>,
810 config: Option<&Config>,
811) -> Result<Keys> {
812 let keys = Keys::generate();
813 write_keys_to_path(&config_dir.join("keys"), &keys)?;
814 seed_identity_defaults_if_needed(data_dir, config);
815 Ok(keys)
816}
817
818pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
820 keys.public_key().to_bytes()
821}
822
823pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
825 use nostr::PublicKey;
826 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
827 Ok(pk.to_bytes())
828}
829
830pub fn generate_auth_cookie() -> Result<(String, String)> {
832 use rand::Rng;
833
834 let cookie_path = get_auth_cookie_path();
835
836 if let Some(parent) = cookie_path.parent() {
838 fs::create_dir_all(parent)?;
839 }
840
841 let mut rng = rand::thread_rng();
843 let username = format!("htree_{}", rng.gen::<u32>());
844 let password: String = (0..32)
845 .map(|_| {
846 let idx = rng.gen_range(0..62);
847 match idx {
848 0..=25 => (b'a' + idx) as char,
849 26..=51 => (b'A' + (idx - 26)) as char,
850 _ => (b'0' + (idx - 52)) as char,
851 }
852 })
853 .collect();
854
855 let content = format!("{}:{}", username, password);
857 fs::write(&cookie_path, content)?;
858
859 #[cfg(unix)]
861 {
862 use std::os::unix::fs::PermissionsExt;
863 let perms = fs::Permissions::from_mode(0o600);
864 fs::set_permissions(&cookie_path, perms)?;
865 }
866
867 Ok((username, password))
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use crate::test_support::{test_env_lock, EnvVarGuard};
874 use tempfile::TempDir;
875
876 #[test]
877 fn test_config_default() {
878 let config = Config::default();
879 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
880 assert!(config.server.enable_auth);
881 assert!(config.server.enable_multicast);
882 assert_eq!(config.server.multicast_group, "239.255.42.98");
883 assert_eq!(config.server.multicast_port, 48555);
884 assert_eq!(config.server.max_multicast_peers, 12);
885 assert!(!config.server.enable_wifi_aware);
886 assert_eq!(config.server.max_wifi_aware_peers, 0);
887 assert!(!config.server.enable_bluetooth);
888 assert_eq!(config.server.max_bluetooth_peers, 0);
889 assert_eq!(config.storage.max_size_gb, 10);
890 assert!(config.storage.evict_orphans);
891 assert!(config.nostr.enabled);
892 assert!(config
893 .nostr
894 .relays
895 .contains(&"wss://upload.iris.to/nostr".to_string()));
896 assert!(config.blossom.enabled);
897 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
898 assert_eq!(config.nostr.max_write_distance, 3);
899 assert_eq!(config.nostr.db_max_size_gb, 10);
900 assert_eq!(config.nostr.spambox_max_size_gb, 1);
901 assert!(!config.nostr.negentropy_only);
902 assert_eq!(config.nostr.overmute_threshold, 1.0);
903 assert_eq!(config.nostr.mirror_kinds, vec![0, 1, 3, 6, 7, 9_735]);
904 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
905 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
906 assert!(config.nostr.history_sync_on_reconnect);
907 assert!(config.nostr.socialgraph_root.is_none());
908 assert_eq!(
909 config.nostr.bootstrap_follows,
910 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
911 );
912 assert!(!config.server.socialgraph_snapshot_public);
913 assert!(config.cashu.accepted_mints.is_empty());
914 assert!(config.cashu.default_mint.is_none());
915 assert_eq!(config.cashu.quote_payment_offer_sat, 3);
916 assert_eq!(config.cashu.quote_ttl_ms, 1_500);
917 assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
918 assert_eq!(config.cashu.mint_failure_block_threshold, 2);
919 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
920 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
921 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
922 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
923 assert_eq!(config.cashu.payment_default_block_threshold, 0);
924 assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
925 }
926
927 #[test]
928 fn test_nostr_config_deserialize_with_defaults() {
929 let toml_str = r#"
930[nostr]
931relays = ["wss://relay.damus.io"]
932"#;
933 let config: Config = toml::from_str(toml_str).unwrap();
934 assert!(config.nostr.enabled);
935 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
936 assert!(config.storage.evict_orphans);
937 assert_eq!(config.nostr.social_graph_crawl_depth, 2);
938 assert_eq!(config.nostr.max_write_distance, 3);
939 assert_eq!(config.nostr.db_max_size_gb, 10);
940 assert_eq!(config.nostr.spambox_max_size_gb, 1);
941 assert!(!config.nostr.negentropy_only);
942 assert_eq!(config.nostr.overmute_threshold, 1.0);
943 assert_eq!(config.nostr.mirror_kinds, vec![0, 1, 3, 6, 7, 9_735]);
944 assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
945 assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
946 assert!(config.nostr.history_sync_on_reconnect);
947 assert!(config.nostr.socialgraph_root.is_none());
948 assert_eq!(
949 config.nostr.bootstrap_follows,
950 vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
951 );
952 }
953
954 #[test]
955 fn test_nostr_config_deserialize_with_socialgraph() {
956 let toml_str = r#"
957[nostr]
958relays = ["wss://relay.damus.io"]
959socialgraph_root = "npub1test"
960bootstrap_follows = []
961social_graph_crawl_depth = 3
962max_write_distance = 5
963negentropy_only = true
964overmute_threshold = 2.5
965mirror_kinds = [0, 10000]
966history_sync_author_chunk_size = 250
967history_sync_per_author_event_limit = 128
968history_sync_on_reconnect = false
969"#;
970 let config: Config = toml::from_str(toml_str).unwrap();
971 assert!(config.nostr.enabled);
972 assert!(config.storage.evict_orphans);
973 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
974 assert!(config.nostr.bootstrap_follows.is_empty());
975 assert_eq!(config.nostr.social_graph_crawl_depth, 3);
976 assert_eq!(config.nostr.max_write_distance, 5);
977 assert_eq!(config.nostr.db_max_size_gb, 10);
978 assert_eq!(config.nostr.spambox_max_size_gb, 1);
979 assert!(config.nostr.negentropy_only);
980 assert_eq!(config.nostr.overmute_threshold, 2.5);
981 assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
982 assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
983 assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
984 assert!(!config.nostr.history_sync_on_reconnect);
985 }
986
987 #[test]
988 fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
989 let toml_str = r#"
990[nostr]
991relays = ["wss://relay.damus.io"]
992crawl_depth = 4
993"#;
994 let config: Config = toml::from_str(toml_str).unwrap();
995 assert_eq!(config.nostr.social_graph_crawl_depth, 4);
996 }
997
998 #[test]
999 fn test_storage_config_disables_orphan_eviction_when_requested() {
1000 let toml_str = r#"
1001[storage]
1002evict_orphans = false
1003"#;
1004 let config: Config = toml::from_str(toml_str).unwrap();
1005 assert!(!config.storage.evict_orphans);
1006 }
1007
1008 #[test]
1009 fn test_server_config_deserialize_with_multicast() {
1010 let toml_str = r#"
1011[server]
1012enable_multicast = true
1013multicast_group = "239.255.42.99"
1014multicast_port = 49001
1015max_multicast_peers = 12
1016enable_wifi_aware = true
1017max_wifi_aware_peers = 5
1018enable_bluetooth = true
1019max_bluetooth_peers = 6
1020"#;
1021 let config: Config = toml::from_str(toml_str).unwrap();
1022 assert!(config.server.enable_multicast);
1023 assert_eq!(config.server.multicast_group, "239.255.42.99");
1024 assert_eq!(config.server.multicast_port, 49_001);
1025 assert_eq!(config.server.max_multicast_peers, 12);
1026 assert!(config.server.enable_wifi_aware);
1027 assert_eq!(config.server.max_wifi_aware_peers, 5);
1028 assert!(config.server.enable_bluetooth);
1029 assert_eq!(config.server.max_bluetooth_peers, 6);
1030 }
1031
1032 #[test]
1033 fn test_cashu_config_deserialize_with_accepted_mints() {
1034 let toml_str = r#"
1035[cashu]
1036accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1037default_mint = "https://mint1.example"
1038quote_payment_offer_sat = 5
1039quote_ttl_ms = 2500
1040settlement_timeout_ms = 7000
1041mint_failure_block_threshold = 3
1042peer_suggested_mint_base_cap_sat = 4
1043peer_suggested_mint_success_step_sat = 2
1044peer_suggested_mint_receipt_step_sat = 3
1045peer_suggested_mint_max_cap_sat = 34
1046payment_default_block_threshold = 2
1047chunk_target_bytes = 65536
1048"#;
1049 let config: Config = toml::from_str(toml_str).unwrap();
1050 assert_eq!(
1051 config.cashu.accepted_mints,
1052 vec![
1053 "https://mint1.example".to_string(),
1054 "http://127.0.0.1:3338".to_string()
1055 ]
1056 );
1057 assert_eq!(
1058 config.cashu.default_mint,
1059 Some("https://mint1.example".to_string())
1060 );
1061 assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1062 assert_eq!(config.cashu.quote_ttl_ms, 2500);
1063 assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1064 assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1065 assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1066 assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1067 assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1068 assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1069 assert_eq!(config.cashu.payment_default_block_threshold, 2);
1070 assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1071 }
1072
1073 #[test]
1074 fn test_auth_cookie_generation() -> Result<()> {
1075 let _lock = test_env_lock()
1076 .lock()
1077 .unwrap_or_else(|err| err.into_inner());
1078 let temp_dir = TempDir::new()?;
1079 let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1080
1081 let (username, password) = generate_auth_cookie()?;
1082
1083 assert!(username.starts_with("htree_"));
1084 assert_eq!(password.len(), 32);
1085
1086 let cookie_path = get_auth_cookie_path();
1088 assert!(cookie_path.exists());
1089
1090 let (u2, p2) = read_auth_cookie()?;
1092 assert_eq!(username, u2);
1093 assert_eq!(password, p2);
1094
1095 Ok(())
1096 }
1097
1098 #[test]
1099 fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1100 let mut config = BlossomConfig::default();
1101 config.servers = vec!["https://legacy.server".to_string()];
1102
1103 let read = config.all_read_servers();
1104 assert!(read.contains(&"https://legacy.server".to_string()));
1105 assert!(read.contains(&"https://cdn.iris.to".to_string()));
1106 assert!(read.contains(&"https://blossom.primal.net".to_string()));
1107 assert!(read.contains(&"https://upload.iris.to".to_string()));
1108
1109 let write = config.all_write_servers();
1110 assert!(write.contains(&"https://legacy.server".to_string()));
1111 assert!(write.contains(&"https://upload.iris.to".to_string()));
1112 }
1113
1114 #[test]
1115 fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1116 let config = BlossomConfig {
1117 enabled: true,
1118 servers: Vec::new(),
1119 read_servers: Vec::new(),
1120 write_servers: Vec::new(),
1121 max_upload_mb: default_max_upload_mb(),
1122 };
1123
1124 let read = config.all_read_servers();
1125 let mut expected = default_read_servers();
1126 expected.extend(default_write_servers());
1127 expected.sort();
1128 expected.dedup();
1129 assert_eq!(read, expected);
1130
1131 let write = config.all_write_servers();
1132 assert_eq!(write, default_write_servers());
1133 }
1134
1135 #[test]
1136 fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1137 let nostr = NostrConfig {
1138 enabled: false,
1139 relays: vec!["wss://relay.example".to_string()],
1140 ..NostrConfig::default()
1141 };
1142 assert!(nostr.active_relays().is_empty());
1143
1144 let blossom = BlossomConfig {
1145 enabled: false,
1146 servers: vec!["https://legacy.server".to_string()],
1147 read_servers: vec!["https://read.example".to_string()],
1148 write_servers: vec!["https://write.example".to_string()],
1149 max_upload_mb: default_max_upload_mb(),
1150 };
1151 assert!(blossom.all_read_servers().is_empty());
1152 assert!(blossom.all_write_servers().is_empty());
1153 }
1154}