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