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