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