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