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