Skip to main content

hashtree_cli/
config.rs

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/// htree self-update preferences. Auto-check is on by default — htree
27/// quietly checks for a newer published binary at most once per
28/// `check_interval_hours` and prints a one-liner to stderr when one
29/// exists. `auto_install` is off by default; flip it on to install in
30/// the background and print the result.
31#[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    /// Port for the built-in STUN server (0 = disabled)
94    #[serde(default = "default_stun_port")]
95    pub stun_port: u16,
96    /// Enable WebRTC P2P connections
97    #[serde(default = "default_enable_webrtc")]
98    pub enable_webrtc: bool,
99    /// Enable FIPS-backed Hashtree blob exchange.
100    #[serde(default = "default_enable_fips")]
101    pub enable_fips: bool,
102    /// FIPS discovery/signaling scope for Hashtree peers.
103    #[serde(default = "default_fips_discovery_scope")]
104    pub fips_discovery_scope: String,
105    /// FIPS Nostr relays used for discovery adverts and encrypted signaling.
106    /// Empty means use active [nostr].relays, then FIPS built-in defaults.
107    #[serde(default)]
108    pub fips_relays: Vec<String>,
109    /// Always-configured Hashtree FIPS peers. These are useful for origin/cache
110    /// pairs that should connect immediately without waiting for discovery.
111    #[serde(
112        default,
113        alias = "preconfigured_fips_peers",
114        alias = "fips_static_peers"
115    )]
116    pub fips_peers: Vec<ConfiguredFipsPeer>,
117    /// Enable ordinary FIPS UDP endpoint transport.
118    #[serde(default = "default_enable_fips_udp")]
119    pub enable_fips_udp: bool,
120    /// FIPS UDP bind address. Empty/default lets the kernel pick an ephemeral port.
121    #[serde(default)]
122    pub fips_udp_bind_addr: Option<String>,
123    /// Advertise the FIPS UDP endpoint as directly reachable.
124    #[serde(default)]
125    pub fips_udp_public: bool,
126    /// Explicit FIPS UDP address to advertise when `fips_udp_public` is true.
127    #[serde(default)]
128    pub fips_udp_external_addr: Option<String>,
129    /// Enable FIPS WebRTC endpoint transport.
130    #[serde(default = "default_enable_fips_webrtc")]
131    pub enable_fips_webrtc: bool,
132    /// Allow daemon cache misses to fetch blobs from FIPS peers.
133    #[serde(default = "default_fetch_from_fips_peers", alias = "http_fips_fetch")]
134    pub fetch_from_fips_peers: bool,
135    /// How long one FIPS blob request waits for a valid response.
136    #[serde(default = "default_fips_request_timeout_ms")]
137    pub fips_request_timeout_ms: u64,
138    /// Allow HTTP misses to fetch blobs from connected WebRTC peers.
139    #[serde(default = "default_http_webrtc_fetch")]
140    pub http_webrtc_fetch: bool,
141    /// Explicit daemon endpoint URLs this node may share privately with connected peers
142    /// for WebRTC signaling handoff.
143    #[serde(default, alias = "peer_direct_urls", alias = "peer_advertise_urls")]
144    pub peer_signal_urls: Vec<String>,
145    /// Enable LAN multicast discovery/signaling for native peers.
146    #[serde(default = "default_enable_multicast")]
147    pub enable_multicast: bool,
148    /// IPv4 multicast group used for LAN discovery/signaling.
149    #[serde(default = "default_multicast_group")]
150    pub multicast_group: String,
151    /// UDP port used for LAN multicast discovery/signaling.
152    #[serde(default = "default_multicast_port")]
153    pub multicast_port: u16,
154    /// Maximum peers admitted from LAN multicast discovery.
155    /// Set to 0 to disable multicast even when enable_multicast is true.
156    #[serde(default = "default_max_multicast_peers")]
157    pub max_multicast_peers: usize,
158    /// Enable Android Wi-Fi Aware nearby discovery/signaling for native peers.
159    #[serde(default = "default_enable_wifi_aware")]
160    pub enable_wifi_aware: bool,
161    /// Maximum peers admitted from Wi-Fi Aware discovery.
162    /// Set to 0 to disable Wi-Fi Aware even when enable_wifi_aware is true.
163    #[serde(default = "default_max_wifi_aware_peers")]
164    pub max_wifi_aware_peers: usize,
165    /// Enable native Bluetooth discovery/transport for nearby peers.
166    #[serde(default = "default_enable_bluetooth")]
167    pub enable_bluetooth: bool,
168    /// Maximum peers admitted from Bluetooth discovery.
169    /// Set to 0 to disable Bluetooth even when enable_bluetooth is true.
170    #[serde(default = "default_max_bluetooth_peers")]
171    pub max_bluetooth_peers: usize,
172    /// Allow anyone with valid Nostr auth to write (default: true)
173    /// When false, only social graph members can write
174    #[serde(default = "default_public_writes")]
175    pub public_writes: bool,
176    /// Allow public plaintext reads from mutable npub routes (default: true)
177    /// When false, only configured or social graph approved npubs are served.
178    #[serde(default = "default_public_plaintext_reads")]
179    pub public_plaintext_reads: bool,
180    /// Allow public access to social graph snapshot endpoint (default: false)
181    #[serde(default = "default_socialgraph_snapshot_public")]
182    pub socialgraph_snapshot_public: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ConfiguredFipsPeer {
187    pub npub: String,
188    #[serde(default)]
189    pub udp_addresses: Vec<String>,
190}
191
192fn default_public_writes() -> bool {
193    true
194}
195
196fn default_public_plaintext_reads() -> bool {
197    true
198}
199
200fn default_socialgraph_snapshot_public() -> bool {
201    false
202}
203
204impl ServerConfig {
205    pub fn resolved_fips_relays(&self, active_nostr_relays: &[String]) -> Vec<String> {
206        let configured = if self.fips_relays.is_empty() {
207            active_nostr_relays
208        } else {
209            &self.fips_relays
210        };
211        merge_fips_signal_relays(configured)
212    }
213}
214
215const DEFAULT_FIPS_SIGNAL_RELAYS: [&str; 2] = ["wss://temp.iris.to", "wss://relay.primal.net"];
216
217fn merge_fips_signal_relays(configured: &[String]) -> Vec<String> {
218    let mut relays = Vec::new();
219    for relay in configured
220        .iter()
221        .map(String::as_str)
222        .chain(DEFAULT_FIPS_SIGNAL_RELAYS)
223    {
224        let normalized = relay.trim().trim_end_matches('/').to_string();
225        if normalized.is_empty() || relays.contains(&normalized) {
226            continue;
227        }
228        relays.push(normalized);
229    }
230    relays
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct StorageConfig {
235    #[serde(default = "default_data_dir")]
236    pub data_dir: String,
237    #[serde(default = "default_max_size_gb")]
238    pub max_size_gb: u64,
239    #[serde(default = "default_storage_evict_orphans")]
240    pub evict_orphans: bool,
241    /// Optional S3/R2 backend for blob storage
242    #[serde(default)]
243    pub s3: Option<S3Config>,
244}
245
246/// S3-compatible storage configuration (works with AWS S3, Cloudflare R2, MinIO, etc.)
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct S3Config {
249    /// S3 endpoint URL (e.g., "https://<account_id>.r2.cloudflarestorage.com" for R2)
250    pub endpoint: String,
251    /// S3 bucket name
252    pub bucket: String,
253    /// Optional key prefix for all blobs (e.g., "blobs/")
254    #[serde(default)]
255    pub prefix: Option<String>,
256    /// AWS region (use "auto" for R2)
257    #[serde(default = "default_s3_region")]
258    pub region: String,
259    /// Access key ID (can also be set via AWS_ACCESS_KEY_ID env var)
260    #[serde(default)]
261    pub access_key: Option<String>,
262    /// Secret access key (can also be set via AWS_SECRET_ACCESS_KEY env var)
263    #[serde(default)]
264    pub secret_key: Option<String>,
265    /// Public URL for serving blobs (optional, for generating public URLs)
266    #[serde(default)]
267    pub public_url: Option<String>,
268}
269
270fn default_s3_region() -> String {
271    "auto".to_string()
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct NostrConfig {
276    #[serde(default = "default_nostr_enabled")]
277    pub enabled: bool,
278    #[serde(default = "default_relays")]
279    pub relays: Vec<String>,
280    /// List of npubs allowed to write (blossom uploads). If empty, uses public_writes setting.
281    #[serde(default)]
282    pub allowed_npubs: Vec<String>,
283    /// Social graph root pubkey (npub). Defaults to own key if not set.
284    #[serde(default)]
285    pub socialgraph_root: Option<String>,
286    /// Pubkeys to seed into contacts.json when a new identity is initialized.
287    /// Set to [] to opt out.
288    #[serde(default = "default_nostr_bootstrap_follows")]
289    pub bootstrap_follows: Vec<String>,
290    /// How many hops to crawl the social graph (default: 2)
291    #[serde(default = "default_social_graph_crawl_depth", alias = "crawl_depth")]
292    pub social_graph_crawl_depth: u32,
293    /// Max follow distance to mirror into public event/profile indexes.
294    /// Defaults to social_graph_crawl_depth when unset.
295    #[serde(default)]
296    pub mirror_max_follow_distance: Option<u32>,
297    /// Max follow distance for write access (default: 3)
298    #[serde(default = "default_max_write_distance")]
299    pub max_write_distance: u32,
300    /// Max size for the trusted social graph store in GB (default: 10)
301    #[serde(default = "default_nostr_db_max_size_gb")]
302    pub db_max_size_gb: u64,
303    /// Max size for the social graph spambox in GB (default: 1)
304    /// Set to 0 for memory-only spambox (no on-disk DB)
305    #[serde(default = "default_nostr_spambox_max_size_gb")]
306    pub spambox_max_size_gb: u64,
307    /// Require relays to support NIP-77 negentropy for mirror history sync.
308    #[serde(default)]
309    pub negentropy_only: bool,
310    /// Threshold for treating a user as overmuted in mirrored profile indexing/search.
311    #[serde(default = "default_nostr_overmute_threshold")]
312    pub overmute_threshold: f64,
313    /// Kinds mirrored from upstream relays for the trusted hashtree index.
314    #[serde(default = "default_nostr_mirror_kinds")]
315    pub mirror_kinds: Vec<u16>,
316    /// How many graph authors to reconcile before checkpointing the mirror root.
317    #[serde(default = "default_nostr_history_sync_author_chunk_size")]
318    pub history_sync_author_chunk_size: usize,
319    /// Maximum mirrored history events to fetch per author during history sync.
320    #[serde(default = "default_nostr_history_sync_per_author_event_limit")]
321    pub history_sync_per_author_event_limit: usize,
322    /// Run a catch-up history sync after relay reconnects.
323    #[serde(default = "default_nostr_history_sync_on_reconnect")]
324    pub history_sync_on_reconnect: bool,
325    /// Fetch complete kind-1 history for authors up to this follow distance.
326    /// Set to null to disable.
327    #[serde(default = "default_nostr_full_text_note_history_follow_distance")]
328    pub full_text_note_history_follow_distance: Option<u32>,
329    /// Maximum relay pages per author for startup text-note history fetches.
330    /// Set to 0 to skip the expensive startup text-note catch-up.
331    #[serde(default = "default_nostr_full_text_note_history_max_relay_pages")]
332    pub full_text_note_history_max_relay_pages: usize,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct BlossomConfig {
337    #[serde(default = "default_blossom_enabled")]
338    pub enabled: bool,
339    /// File servers for push/pull (legacy, both read and write)
340    #[serde(default)]
341    pub servers: Vec<String>,
342    /// Read-only file servers (fallback for fetching content)
343    #[serde(default = "default_read_servers")]
344    pub read_servers: Vec<String>,
345    /// Write-enabled file servers (for uploading)
346    #[serde(default = "default_write_servers")]
347    pub write_servers: Vec<String>,
348    /// Maximum upload size in MB (default: 5)
349    #[serde(default = "default_max_upload_mb")]
350    pub max_upload_mb: u64,
351    /// Require public Blossom and peer-fetched cached blobs to look encrypted.
352    #[serde(default = "default_require_random_untrusted_ingest")]
353    pub require_random_untrusted_ingest: bool,
354    /// Return from Blossom PUT /upload after validation and queue local storage
355    /// writes in the background.
356    #[serde(default = "default_optimistic_uploads")]
357    pub optimistic_uploads: bool,
358    /// Background write-behind targets for blobs accepted by this server.
359    /// Useful for hot-cache origins that should ACK local writes quickly and
360    /// replicate them to a larger origin without blocking the client.
361    #[serde(
362        default,
363        alias = "write_behind_servers",
364        alias = "mirror_write_servers"
365    )]
366    pub replicate_servers: Vec<String>,
367    /// Maximum in-memory upload body bytes waiting for background replication.
368    #[serde(default = "default_replicate_queue_mb")]
369    pub replicate_queue_mb: u64,
370}
371
372impl BlossomConfig {
373    pub fn all_read_servers(&self) -> Vec<String> {
374        if !self.enabled {
375            return Vec::new();
376        }
377        let mut servers = self.servers.clone();
378        servers.extend(self.read_servers.clone());
379        servers.extend(self.write_servers.clone());
380        if servers.is_empty() {
381            servers = default_read_servers();
382            servers.extend(default_write_servers());
383        }
384        servers.sort();
385        servers.dedup();
386        servers
387    }
388
389    pub fn all_write_servers(&self) -> Vec<String> {
390        if !self.enabled {
391            return Vec::new();
392        }
393        let mut servers = self.servers.clone();
394        servers.extend(self.write_servers.clone());
395        if servers.is_empty() {
396            servers = default_write_servers();
397        }
398        servers.sort();
399        servers.dedup();
400        servers
401    }
402}
403
404impl NostrConfig {
405    pub fn active_relays(&self) -> Vec<String> {
406        if self.enabled {
407            self.relays.clone()
408        } else {
409            Vec::new()
410        }
411    }
412}
413
414// Keep in sync with hashtree-config/src/lib.rs
415fn default_read_servers() -> Vec<String> {
416    let mut servers = vec![
417        "https://blossom.primal.net".to_string(),
418        "https://cdn.iris.to".to_string(),
419    ];
420    servers.sort();
421    servers
422}
423
424fn default_write_servers() -> Vec<String> {
425    vec!["https://upload.iris.to".to_string()]
426}
427
428fn default_max_upload_mb() -> u64 {
429    5
430}
431
432fn default_require_random_untrusted_ingest() -> bool {
433    true
434}
435
436fn default_optimistic_uploads() -> bool {
437    false
438}
439
440fn default_replicate_queue_mb() -> u64 {
441    512
442}
443
444fn default_nostr_enabled() -> bool {
445    true
446}
447
448fn default_blossom_enabled() -> bool {
449    true
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct SyncConfig {
454    /// Enable background sync (auto-pull trees)
455    #[serde(default = "default_sync_enabled")]
456    pub enabled: bool,
457    /// Sync own trees (subscribed via Nostr)
458    #[serde(default = "default_sync_own")]
459    pub sync_own: bool,
460    /// Sync followed users' public trees
461    #[serde(default = "default_sync_followed")]
462    pub sync_followed: bool,
463    /// Max concurrent sync tasks
464    #[serde(default = "default_max_concurrent")]
465    pub max_concurrent: usize,
466    /// WebRTC request timeout in milliseconds
467    #[serde(default = "default_webrtc_timeout_ms")]
468    pub webrtc_timeout_ms: u64,
469    /// Blossom request timeout in milliseconds
470    #[serde(default = "default_blossom_timeout_ms")]
471    pub blossom_timeout_ms: u64,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct CashuConfig {
476    /// Cashu mint base URLs we accept for bandwidth incentives.
477    #[serde(default)]
478    pub accepted_mints: Vec<String>,
479    /// Default mint to use for wallet operations.
480    #[serde(default)]
481    pub default_mint: Option<String>,
482    /// Default post-delivery payment offer for quoted retrievals.
483    #[serde(default = "default_cashu_quote_payment_offer_sat")]
484    pub quote_payment_offer_sat: u64,
485    /// Quote validity window in milliseconds.
486    #[serde(default = "default_cashu_quote_ttl_ms")]
487    pub quote_ttl_ms: u32,
488    /// Maximum time to wait for post-delivery settlement before recording a default.
489    #[serde(default = "default_cashu_settlement_timeout_ms")]
490    pub settlement_timeout_ms: u64,
491    /// Block mints whose failed redemptions keep outnumbering successful redemptions.
492    #[serde(default = "default_cashu_mint_failure_block_threshold")]
493    pub mint_failure_block_threshold: u64,
494    /// Base cap for trying a peer-suggested mint we do not already trust.
495    #[serde(default = "default_cashu_peer_suggested_mint_base_cap_sat")]
496    pub peer_suggested_mint_base_cap_sat: u64,
497    /// Additional cap granted per successful delivery from that peer.
498    #[serde(default = "default_cashu_peer_suggested_mint_success_step_sat")]
499    pub peer_suggested_mint_success_step_sat: u64,
500    /// Additional cap granted per settled payment received from that peer.
501    #[serde(default = "default_cashu_peer_suggested_mint_receipt_step_sat")]
502    pub peer_suggested_mint_receipt_step_sat: u64,
503    /// Hard ceiling for untrusted peer-suggested mint exposure.
504    #[serde(default = "default_cashu_peer_suggested_mint_max_cap_sat")]
505    pub peer_suggested_mint_max_cap_sat: u64,
506    /// Block serving peers whose unpaid defaults reach this threshold.
507    #[serde(default)]
508    pub payment_default_block_threshold: u64,
509    /// Target chunk size for quoted paid delivery.
510    #[serde(default = "default_cashu_chunk_target_bytes")]
511    pub chunk_target_bytes: usize,
512}
513
514impl Default for CashuConfig {
515    fn default() -> Self {
516        Self {
517            accepted_mints: Vec::new(),
518            default_mint: None,
519            quote_payment_offer_sat: default_cashu_quote_payment_offer_sat(),
520            quote_ttl_ms: default_cashu_quote_ttl_ms(),
521            settlement_timeout_ms: default_cashu_settlement_timeout_ms(),
522            mint_failure_block_threshold: default_cashu_mint_failure_block_threshold(),
523            peer_suggested_mint_base_cap_sat: default_cashu_peer_suggested_mint_base_cap_sat(),
524            peer_suggested_mint_success_step_sat:
525                default_cashu_peer_suggested_mint_success_step_sat(),
526            peer_suggested_mint_receipt_step_sat:
527                default_cashu_peer_suggested_mint_receipt_step_sat(),
528            peer_suggested_mint_max_cap_sat: default_cashu_peer_suggested_mint_max_cap_sat(),
529            payment_default_block_threshold: 0,
530            chunk_target_bytes: default_cashu_chunk_target_bytes(),
531        }
532    }
533}
534
535fn default_cashu_quote_payment_offer_sat() -> u64 {
536    3
537}
538
539fn default_cashu_quote_ttl_ms() -> u32 {
540    1_500
541}
542
543fn default_cashu_settlement_timeout_ms() -> u64 {
544    5_000
545}
546
547fn default_cashu_mint_failure_block_threshold() -> u64 {
548    2
549}
550
551fn default_cashu_peer_suggested_mint_base_cap_sat() -> u64 {
552    3
553}
554
555fn default_cashu_peer_suggested_mint_success_step_sat() -> u64 {
556    1
557}
558
559fn default_cashu_peer_suggested_mint_receipt_step_sat() -> u64 {
560    2
561}
562
563fn default_cashu_peer_suggested_mint_max_cap_sat() -> u64 {
564    21
565}
566
567fn default_cashu_chunk_target_bytes() -> usize {
568    32 * 1024
569}
570
571fn default_sync_enabled() -> bool {
572    true
573}
574
575fn default_sync_own() -> bool {
576    true
577}
578
579fn default_sync_followed() -> bool {
580    true
581}
582
583fn default_max_concurrent() -> usize {
584    3
585}
586
587fn default_webrtc_timeout_ms() -> u64 {
588    2000
589}
590
591fn default_blossom_timeout_ms() -> u64 {
592    10000
593}
594
595fn default_social_graph_crawl_depth() -> u32 {
596    2
597}
598
599fn default_nostr_bootstrap_follows() -> Vec<String> {
600    vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
601}
602
603fn default_max_write_distance() -> u32 {
604    3
605}
606
607fn default_nostr_db_max_size_gb() -> u64 {
608    10
609}
610
611fn default_nostr_spambox_max_size_gb() -> u64 {
612    1
613}
614
615fn default_nostr_history_sync_on_reconnect() -> bool {
616    true
617}
618
619fn default_nostr_overmute_threshold() -> f64 {
620    1.0
621}
622
623fn default_nostr_mirror_kinds() -> Vec<u16> {
624    vec![0, 1, 3, 6, 7, 9_735, 30_023]
625}
626
627fn default_nostr_history_sync_author_chunk_size() -> usize {
628    5_000
629}
630
631fn default_nostr_history_sync_per_author_event_limit() -> usize {
632    256
633}
634
635fn default_nostr_full_text_note_history_follow_distance() -> Option<u32> {
636    Some(2)
637}
638
639fn default_nostr_full_text_note_history_max_relay_pages() -> usize {
640    0
641}
642
643fn default_relays() -> Vec<String> {
644    vec![
645        "wss://nos.lol".to_string(),
646        "wss://relay.snort.social".to_string(),
647        "wss://temp.iris.to".to_string(),
648    ]
649}
650
651fn default_bind_address() -> String {
652    "127.0.0.1:8080".to_string()
653}
654
655fn default_enable_auth() -> bool {
656    true
657}
658
659fn default_stun_port() -> u16 {
660    3478 // Standard STUN port (RFC 5389)
661}
662
663fn default_enable_webrtc() -> bool {
664    true
665}
666
667fn default_enable_fips() -> bool {
668    true
669}
670
671fn default_fips_discovery_scope() -> String {
672    "hashtree-v1".to_string()
673}
674
675fn default_enable_fips_udp() -> bool {
676    true
677}
678
679fn default_enable_fips_webrtc() -> bool {
680    true
681}
682
683fn default_fetch_from_fips_peers() -> bool {
684    true
685}
686
687fn default_fips_request_timeout_ms() -> u64 {
688    5_500
689}
690
691fn default_http_webrtc_fetch() -> bool {
692    true
693}
694
695fn default_enable_multicast() -> bool {
696    true
697}
698
699fn default_multicast_group() -> String {
700    "239.255.42.98".to_string()
701}
702
703fn default_multicast_port() -> u16 {
704    48555
705}
706
707fn default_max_multicast_peers() -> usize {
708    12
709}
710
711fn default_enable_wifi_aware() -> bool {
712    false
713}
714
715fn default_max_wifi_aware_peers() -> usize {
716    0
717}
718
719fn default_enable_bluetooth() -> bool {
720    false
721}
722
723fn default_max_bluetooth_peers() -> usize {
724    0
725}
726
727fn default_data_dir() -> String {
728    hashtree_config::get_hashtree_dir()
729        .join("data")
730        .to_string_lossy()
731        .to_string()
732}
733
734fn default_max_size_gb() -> u64 {
735    10
736}
737
738fn default_storage_evict_orphans() -> bool {
739    true
740}
741
742impl Default for ServerConfig {
743    fn default() -> Self {
744        Self {
745            mode: ServerMode::default(),
746            bind_address: default_bind_address(),
747            enable_auth: default_enable_auth(),
748            stun_port: default_stun_port(),
749            enable_webrtc: default_enable_webrtc(),
750            enable_fips: default_enable_fips(),
751            fips_discovery_scope: default_fips_discovery_scope(),
752            fips_relays: Vec::new(),
753            fips_peers: Vec::new(),
754            enable_fips_udp: default_enable_fips_udp(),
755            fips_udp_bind_addr: None,
756            fips_udp_public: false,
757            fips_udp_external_addr: None,
758            enable_fips_webrtc: default_enable_fips_webrtc(),
759            fetch_from_fips_peers: default_fetch_from_fips_peers(),
760            fips_request_timeout_ms: default_fips_request_timeout_ms(),
761            http_webrtc_fetch: default_http_webrtc_fetch(),
762            peer_signal_urls: Vec::new(),
763            enable_multicast: default_enable_multicast(),
764            multicast_group: default_multicast_group(),
765            multicast_port: default_multicast_port(),
766            max_multicast_peers: default_max_multicast_peers(),
767            enable_wifi_aware: default_enable_wifi_aware(),
768            max_wifi_aware_peers: default_max_wifi_aware_peers(),
769            enable_bluetooth: default_enable_bluetooth(),
770            max_bluetooth_peers: default_max_bluetooth_peers(),
771            public_writes: default_public_writes(),
772            public_plaintext_reads: default_public_plaintext_reads(),
773            socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
774        }
775    }
776}
777
778impl Default for StorageConfig {
779    fn default() -> Self {
780        Self {
781            data_dir: default_data_dir(),
782            max_size_gb: default_max_size_gb(),
783            evict_orphans: default_storage_evict_orphans(),
784            s3: None,
785        }
786    }
787}
788
789impl Default for NostrConfig {
790    fn default() -> Self {
791        Self {
792            enabled: default_nostr_enabled(),
793            relays: default_relays(),
794            allowed_npubs: Vec::new(),
795            socialgraph_root: None,
796            bootstrap_follows: default_nostr_bootstrap_follows(),
797            social_graph_crawl_depth: default_social_graph_crawl_depth(),
798            mirror_max_follow_distance: None,
799            max_write_distance: default_max_write_distance(),
800            db_max_size_gb: default_nostr_db_max_size_gb(),
801            spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
802            negentropy_only: false,
803            overmute_threshold: default_nostr_overmute_threshold(),
804            mirror_kinds: default_nostr_mirror_kinds(),
805            history_sync_author_chunk_size: default_nostr_history_sync_author_chunk_size(),
806            history_sync_per_author_event_limit: default_nostr_history_sync_per_author_event_limit(
807            ),
808            history_sync_on_reconnect: default_nostr_history_sync_on_reconnect(),
809            full_text_note_history_follow_distance:
810                default_nostr_full_text_note_history_follow_distance(),
811            full_text_note_history_max_relay_pages:
812                default_nostr_full_text_note_history_max_relay_pages(),
813        }
814    }
815}
816
817impl Default for BlossomConfig {
818    fn default() -> Self {
819        Self {
820            enabled: default_blossom_enabled(),
821            servers: Vec::new(),
822            read_servers: default_read_servers(),
823            write_servers: default_write_servers(),
824            max_upload_mb: default_max_upload_mb(),
825            require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
826            optimistic_uploads: default_optimistic_uploads(),
827            replicate_servers: Vec::new(),
828            replicate_queue_mb: default_replicate_queue_mb(),
829        }
830    }
831}
832
833impl Default for SyncConfig {
834    fn default() -> Self {
835        Self {
836            enabled: default_sync_enabled(),
837            sync_own: default_sync_own(),
838            sync_followed: default_sync_followed(),
839            max_concurrent: default_max_concurrent(),
840            webrtc_timeout_ms: default_webrtc_timeout_ms(),
841            blossom_timeout_ms: default_blossom_timeout_ms(),
842        }
843    }
844}
845
846impl Config {
847    /// Load config from file, or create default if doesn't exist
848    pub fn load() -> Result<Self> {
849        let config_path = get_config_path();
850
851        if config_path.exists() {
852            let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
853            toml::from_str(&content).context("Failed to parse config file")
854        } else {
855            let config = Config::default();
856            config.save()?;
857            Ok(config)
858        }
859    }
860
861    /// Save config to file
862    pub fn save(&self) -> Result<()> {
863        let config_path = get_config_path();
864
865        // Ensure parent directory exists
866        if let Some(parent) = config_path.parent() {
867            fs::create_dir_all(parent)?;
868        }
869
870        let content = toml::to_string_pretty(self)?;
871        fs::write(&config_path, content)?;
872
873        Ok(())
874    }
875}
876
877// Re-export path functions from hashtree_config
878pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
879
880fn read_keys_from_path(keys_path: &Path) -> Result<Keys> {
881    let content = fs::read_to_string(keys_path).context("Failed to read keys file")?;
882    let entries = hashtree_config::parse_keys_file(&content);
883    let nsec_str = entries
884        .into_iter()
885        .next()
886        .map(|e| e.secret)
887        .context("Keys file is empty")?;
888    let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
889    Ok(Keys::new(secret_key))
890}
891
892fn seed_identity_defaults_if_needed(data_dir: Option<&Path>, config: Option<&Config>) {
893    if let (Some(data_dir), Some(config)) = (data_dir, config) {
894        let _ = crate::bootstrap::seed_identity_defaults(data_dir, config);
895    }
896}
897
898fn write_keys_to_path(keys_path: &Path, keys: &Keys) -> Result<()> {
899    if let Some(parent) = keys_path.parent() {
900        fs::create_dir_all(parent)?;
901    }
902
903    let nsec = keys
904        .secret_key()
905        .to_bech32()
906        .context("Failed to encode nsec")?;
907    fs::write(keys_path, &nsec)?;
908
909    #[cfg(unix)]
910    {
911        use std::os::unix::fs::PermissionsExt;
912        let perms = fs::Permissions::from_mode(0o600);
913        fs::set_permissions(keys_path, perms)?;
914    }
915
916    Ok(())
917}
918
919/// Generate and save auth cookie if it doesn't exist
920pub fn ensure_auth_cookie() -> Result<(String, String)> {
921    let cookie_path = get_auth_cookie_path();
922
923    if cookie_path.exists() {
924        read_auth_cookie()
925    } else {
926        generate_auth_cookie()
927    }
928}
929
930/// Read existing auth cookie
931pub fn read_auth_cookie() -> Result<(String, String)> {
932    let cookie_path = get_auth_cookie_path();
933    let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
934
935    let parts: Vec<&str> = content.trim().split(':').collect();
936    if parts.len() != 2 {
937        anyhow::bail!("Invalid auth cookie format");
938    }
939
940    Ok((parts[0].to_string(), parts[1].to_string()))
941}
942
943/// Ensure keys file exists, generating one if not present
944/// Returns (Keys, was_generated)
945pub fn ensure_keys() -> Result<(Keys, bool)> {
946    let config_dir = get_hashtree_dir();
947    let config = Config::load().ok();
948    let data_dir = config
949        .as_ref()
950        .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
951    ensure_keys_in(&config_dir, data_dir, config.as_ref())
952}
953
954/// Ensure keys exist inside an explicit config directory.
955/// Returns (Keys, was_generated)
956pub fn ensure_keys_in(
957    config_dir: &Path,
958    data_dir: Option<&Path>,
959    config: Option<&Config>,
960) -> Result<(Keys, bool)> {
961    let keys_path = config_dir.join("keys");
962
963    if keys_path.exists() {
964        Ok((read_keys_from_path(&keys_path)?, false))
965    } else {
966        let keys = generate_keys_in(config_dir, data_dir, config)?;
967        Ok((keys, true))
968    }
969}
970
971/// Read existing keys
972pub fn read_keys() -> Result<Keys> {
973    read_keys_in(&get_hashtree_dir())
974}
975
976/// Read keys from an explicit config directory.
977pub fn read_keys_in(config_dir: &Path) -> Result<Keys> {
978    read_keys_from_path(&config_dir.join("keys"))
979}
980
981/// Get nsec string, ensuring keys file exists (generate if needed)
982/// Returns (nsec_string, was_generated)
983pub fn ensure_keys_string() -> Result<(String, bool)> {
984    let config_dir = get_hashtree_dir();
985    let config = Config::load().ok();
986    let data_dir = config
987        .as_ref()
988        .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
989    ensure_keys_string_in(&config_dir, data_dir, config.as_ref())
990}
991
992/// Ensure key material exists inside an explicit config directory.
993/// Returns (nsec_string, was_generated)
994pub fn ensure_keys_string_in(
995    config_dir: &Path,
996    data_dir: Option<&Path>,
997    config: Option<&Config>,
998) -> Result<(String, bool)> {
999    let keys_path = config_dir.join("keys");
1000
1001    if keys_path.exists() {
1002        let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
1003        let entries = hashtree_config::parse_keys_file(&content);
1004        let nsec_str = entries
1005            .into_iter()
1006            .next()
1007            .map(|e| e.secret)
1008            .context("Keys file is empty")?;
1009        Ok((nsec_str, false))
1010    } else {
1011        let keys = generate_keys_in(config_dir, data_dir, config)?;
1012        let nsec = keys
1013            .secret_key()
1014            .to_bech32()
1015            .context("Failed to encode nsec")?;
1016        Ok((nsec, true))
1017    }
1018}
1019
1020/// Generate new keys and save to file
1021pub fn generate_keys() -> Result<Keys> {
1022    let config_dir = get_hashtree_dir();
1023    let config = Config::load().ok();
1024    let data_dir = config
1025        .as_ref()
1026        .map(|cfg| Path::new(cfg.storage.data_dir.as_str()));
1027    generate_keys_in(&config_dir, data_dir, config.as_ref())
1028}
1029
1030/// Generate new keys in an explicit config directory and optionally seed
1031/// identity defaults into a caller-owned data directory.
1032pub fn generate_keys_in(
1033    config_dir: &Path,
1034    data_dir: Option<&Path>,
1035    config: Option<&Config>,
1036) -> Result<Keys> {
1037    let keys = Keys::generate();
1038    write_keys_to_path(&config_dir.join("keys"), &keys)?;
1039    seed_identity_defaults_if_needed(data_dir, config);
1040    Ok(keys)
1041}
1042
1043/// Get 32-byte pubkey bytes from Keys.
1044pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
1045    keys.public_key().to_bytes()
1046}
1047
1048/// Parse npub to 32-byte pubkey
1049pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
1050    use nostr::PublicKey;
1051    let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
1052    Ok(pk.to_bytes())
1053}
1054
1055/// Generate new random auth cookie
1056pub fn generate_auth_cookie() -> Result<(String, String)> {
1057    use rand::Rng;
1058
1059    let cookie_path = get_auth_cookie_path();
1060
1061    // Ensure parent directory exists
1062    if let Some(parent) = cookie_path.parent() {
1063        fs::create_dir_all(parent)?;
1064    }
1065
1066    // Generate random credentials
1067    let mut rng = rand::thread_rng();
1068    let username = format!("htree_{}", rng.gen::<u32>());
1069    let password: String = (0..32)
1070        .map(|_| {
1071            let idx = rng.gen_range(0..62);
1072            match idx {
1073                0..=25 => (b'a' + idx) as char,
1074                26..=51 => (b'A' + (idx - 26)) as char,
1075                _ => (b'0' + (idx - 52)) as char,
1076            }
1077        })
1078        .collect();
1079
1080    // Save to file
1081    let content = format!("{}:{}", username, password);
1082    fs::write(&cookie_path, content)?;
1083
1084    // Set permissions to 0600 (owner read/write only)
1085    #[cfg(unix)]
1086    {
1087        use std::os::unix::fs::PermissionsExt;
1088        let perms = fs::Permissions::from_mode(0o600);
1089        fs::set_permissions(&cookie_path, perms)?;
1090    }
1091
1092    Ok((username, password))
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097    use super::*;
1098    use crate::test_support::{test_env_lock, EnvVarGuard};
1099    use tempfile::TempDir;
1100
1101    #[test]
1102    fn test_config_default() {
1103        let config = Config::default();
1104        assert_eq!(config.server.bind_address, "127.0.0.1:8080");
1105        assert!(config.server.enable_auth);
1106        assert!(config.server.enable_multicast);
1107        assert_eq!(config.server.multicast_group, "239.255.42.98");
1108        assert_eq!(config.server.multicast_port, 48555);
1109        assert_eq!(config.server.max_multicast_peers, 12);
1110        assert!(!config.server.enable_wifi_aware);
1111        assert_eq!(config.server.max_wifi_aware_peers, 0);
1112        assert!(!config.server.enable_bluetooth);
1113        assert_eq!(config.server.max_bluetooth_peers, 0);
1114        assert!(config.server.public_plaintext_reads);
1115        assert_eq!(config.storage.max_size_gb, 10);
1116        assert!(config.storage.evict_orphans);
1117        assert!(config.nostr.enabled);
1118        assert!(config
1119            .nostr
1120            .relays
1121            .contains(&"wss://temp.iris.to".to_string()));
1122        assert!(config.blossom.enabled);
1123        assert!(!config.blossom.optimistic_uploads);
1124        assert!(config.blossom.replicate_servers.is_empty());
1125        assert_eq!(config.blossom.replicate_queue_mb, 512);
1126        assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1127        assert_eq!(config.nostr.mirror_max_follow_distance, None);
1128        assert_eq!(config.nostr.max_write_distance, 3);
1129        assert_eq!(config.nostr.db_max_size_gb, 10);
1130        assert_eq!(config.nostr.spambox_max_size_gb, 1);
1131        assert!(!config.nostr.negentropy_only);
1132        assert_eq!(config.nostr.overmute_threshold, 1.0);
1133        assert_eq!(
1134            config.nostr.mirror_kinds,
1135            vec![0, 1, 3, 6, 7, 9_735, 30_023]
1136        );
1137        assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1138        assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1139        assert!(config.nostr.history_sync_on_reconnect);
1140        assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1141        assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1142        assert!(config.nostr.socialgraph_root.is_none());
1143        assert_eq!(
1144            config.nostr.bootstrap_follows,
1145            vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1146        );
1147        assert!(!config.server.socialgraph_snapshot_public);
1148        assert!(config.cashu.accepted_mints.is_empty());
1149        assert!(config.cashu.default_mint.is_none());
1150        assert_eq!(config.cashu.quote_payment_offer_sat, 3);
1151        assert_eq!(config.cashu.quote_ttl_ms, 1_500);
1152        assert_eq!(config.cashu.settlement_timeout_ms, 5_000);
1153        assert_eq!(config.cashu.mint_failure_block_threshold, 2);
1154        assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 3);
1155        assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 1);
1156        assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 2);
1157        assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 21);
1158        assert_eq!(config.cashu.payment_default_block_threshold, 0);
1159        assert_eq!(config.cashu.chunk_target_bytes, 32 * 1024);
1160    }
1161
1162    #[test]
1163    fn test_blossom_optimistic_uploads_deserialize() {
1164        let toml_str = r#"
1165[blossom]
1166optimistic_uploads = true
1167"#;
1168        let config: Config = toml::from_str(toml_str).unwrap();
1169        assert!(config.blossom.optimistic_uploads);
1170        assert!(config.blossom.require_random_untrusted_ingest);
1171    }
1172
1173    #[test]
1174    fn test_blossom_replication_deserialize() {
1175        let toml_str = r#"
1176[blossom]
1177replicate_servers = ["http://127.0.0.1:8081"]
1178replicate_queue_mb = 128
1179"#;
1180        let config: Config = toml::from_str(toml_str).unwrap();
1181        assert_eq!(config.blossom.replicate_servers, ["http://127.0.0.1:8081"]);
1182        assert_eq!(config.blossom.replicate_queue_mb, 128);
1183    }
1184
1185    #[test]
1186    fn test_server_public_plaintext_reads_deserialize() {
1187        let toml_str = r#"
1188[server]
1189public_plaintext_reads = false
1190"#;
1191        let config: Config = toml::from_str(toml_str).unwrap();
1192        assert!(!config.server.public_plaintext_reads);
1193        assert!(config.server.public_writes);
1194    }
1195
1196    #[test]
1197    fn test_nostr_config_deserialize_with_defaults() {
1198        let toml_str = r#"
1199[nostr]
1200relays = ["wss://relay.damus.io"]
1201"#;
1202        let config: Config = toml::from_str(toml_str).unwrap();
1203        assert!(config.nostr.enabled);
1204        assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
1205        assert!(config.storage.evict_orphans);
1206        assert_eq!(config.nostr.social_graph_crawl_depth, 2);
1207        assert_eq!(config.nostr.mirror_max_follow_distance, None);
1208        assert_eq!(config.nostr.max_write_distance, 3);
1209        assert_eq!(config.nostr.db_max_size_gb, 10);
1210        assert_eq!(config.nostr.spambox_max_size_gb, 1);
1211        assert!(!config.nostr.negentropy_only);
1212        assert_eq!(config.nostr.overmute_threshold, 1.0);
1213        assert_eq!(
1214            config.nostr.mirror_kinds,
1215            vec![0, 1, 3, 6, 7, 9_735, 30_023]
1216        );
1217        assert_eq!(config.nostr.history_sync_author_chunk_size, 5_000);
1218        assert_eq!(config.nostr.history_sync_per_author_event_limit, 256);
1219        assert!(config.nostr.history_sync_on_reconnect);
1220        assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(2));
1221        assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 0);
1222        assert!(config.nostr.socialgraph_root.is_none());
1223        assert_eq!(
1224            config.nostr.bootstrap_follows,
1225            vec![hashtree_config::DEFAULT_SOCIALGRAPH_ENTRYPOINT_NPUB.to_string()]
1226        );
1227    }
1228
1229    #[test]
1230    fn test_nostr_config_deserialize_with_socialgraph() {
1231        let toml_str = r#"
1232[nostr]
1233relays = ["wss://relay.damus.io"]
1234socialgraph_root = "npub1test"
1235bootstrap_follows = []
1236social_graph_crawl_depth = 3
1237mirror_max_follow_distance = 2
1238max_write_distance = 5
1239negentropy_only = true
1240overmute_threshold = 2.5
1241mirror_kinds = [0, 10000]
1242history_sync_author_chunk_size = 250
1243history_sync_per_author_event_limit = 128
1244history_sync_on_reconnect = false
1245full_text_note_history_follow_distance = 1
1246full_text_note_history_max_relay_pages = 64
1247"#;
1248        let config: Config = toml::from_str(toml_str).unwrap();
1249        assert!(config.nostr.enabled);
1250        assert!(config.storage.evict_orphans);
1251        assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
1252        assert!(config.nostr.bootstrap_follows.is_empty());
1253        assert_eq!(config.nostr.social_graph_crawl_depth, 3);
1254        assert_eq!(config.nostr.mirror_max_follow_distance, Some(2));
1255        assert_eq!(config.nostr.max_write_distance, 5);
1256        assert_eq!(config.nostr.db_max_size_gb, 10);
1257        assert_eq!(config.nostr.spambox_max_size_gb, 1);
1258        assert!(config.nostr.negentropy_only);
1259        assert_eq!(config.nostr.overmute_threshold, 2.5);
1260        assert_eq!(config.nostr.mirror_kinds, vec![0, 10_000]);
1261        assert_eq!(config.nostr.history_sync_author_chunk_size, 250);
1262        assert_eq!(config.nostr.history_sync_per_author_event_limit, 128);
1263        assert!(!config.nostr.history_sync_on_reconnect);
1264        assert_eq!(config.nostr.full_text_note_history_follow_distance, Some(1));
1265        assert_eq!(config.nostr.full_text_note_history_max_relay_pages, 64);
1266    }
1267
1268    #[test]
1269    fn test_nostr_config_deserialize_legacy_crawl_depth_alias() {
1270        let toml_str = r#"
1271[nostr]
1272relays = ["wss://relay.damus.io"]
1273crawl_depth = 4
1274"#;
1275        let config: Config = toml::from_str(toml_str).unwrap();
1276        assert_eq!(config.nostr.social_graph_crawl_depth, 4);
1277    }
1278
1279    #[test]
1280    fn test_storage_config_disables_orphan_eviction_when_requested() {
1281        let toml_str = r#"
1282[storage]
1283evict_orphans = false
1284"#;
1285        let config: Config = toml::from_str(toml_str).unwrap();
1286        assert!(!config.storage.evict_orphans);
1287    }
1288
1289    #[test]
1290    fn test_server_config_deserialize_with_multicast() {
1291        let toml_str = r#"
1292[server]
1293enable_multicast = true
1294multicast_group = "239.255.42.99"
1295multicast_port = 49001
1296max_multicast_peers = 12
1297enable_wifi_aware = true
1298max_wifi_aware_peers = 5
1299enable_bluetooth = true
1300max_bluetooth_peers = 6
1301"#;
1302        let config: Config = toml::from_str(toml_str).unwrap();
1303        assert!(config.server.enable_multicast);
1304        assert_eq!(config.server.multicast_group, "239.255.42.99");
1305        assert_eq!(config.server.multicast_port, 49_001);
1306        assert_eq!(config.server.max_multicast_peers, 12);
1307        assert!(config.server.enable_wifi_aware);
1308        assert_eq!(config.server.max_wifi_aware_peers, 5);
1309        assert!(config.server.enable_bluetooth);
1310        assert_eq!(config.server.max_bluetooth_peers, 6);
1311    }
1312
1313    #[test]
1314    fn test_cashu_config_deserialize_with_accepted_mints() {
1315        let toml_str = r#"
1316[cashu]
1317accepted_mints = ["https://mint1.example", "http://127.0.0.1:3338"]
1318default_mint = "https://mint1.example"
1319quote_payment_offer_sat = 5
1320quote_ttl_ms = 2500
1321settlement_timeout_ms = 7000
1322mint_failure_block_threshold = 3
1323peer_suggested_mint_base_cap_sat = 4
1324peer_suggested_mint_success_step_sat = 2
1325peer_suggested_mint_receipt_step_sat = 3
1326peer_suggested_mint_max_cap_sat = 34
1327payment_default_block_threshold = 2
1328chunk_target_bytes = 65536
1329"#;
1330        let config: Config = toml::from_str(toml_str).unwrap();
1331        assert_eq!(
1332            config.cashu.accepted_mints,
1333            vec![
1334                "https://mint1.example".to_string(),
1335                "http://127.0.0.1:3338".to_string()
1336            ]
1337        );
1338        assert_eq!(
1339            config.cashu.default_mint,
1340            Some("https://mint1.example".to_string())
1341        );
1342        assert_eq!(config.cashu.quote_payment_offer_sat, 5);
1343        assert_eq!(config.cashu.quote_ttl_ms, 2500);
1344        assert_eq!(config.cashu.settlement_timeout_ms, 7_000);
1345        assert_eq!(config.cashu.mint_failure_block_threshold, 3);
1346        assert_eq!(config.cashu.peer_suggested_mint_base_cap_sat, 4);
1347        assert_eq!(config.cashu.peer_suggested_mint_success_step_sat, 2);
1348        assert_eq!(config.cashu.peer_suggested_mint_receipt_step_sat, 3);
1349        assert_eq!(config.cashu.peer_suggested_mint_max_cap_sat, 34);
1350        assert_eq!(config.cashu.payment_default_block_threshold, 2);
1351        assert_eq!(config.cashu.chunk_target_bytes, 65_536);
1352    }
1353
1354    #[test]
1355    fn test_auth_cookie_generation() -> Result<()> {
1356        let _lock = test_env_lock()
1357            .lock()
1358            .unwrap_or_else(|err| err.into_inner());
1359        let temp_dir = TempDir::new()?;
1360        let _guard = EnvVarGuard::set("HTREE_CONFIG_DIR", temp_dir.path());
1361
1362        let (username, password) = generate_auth_cookie()?;
1363
1364        assert!(username.starts_with("htree_"));
1365        assert_eq!(password.len(), 32);
1366
1367        // Verify cookie file exists
1368        let cookie_path = get_auth_cookie_path();
1369        assert!(cookie_path.exists());
1370
1371        // Verify reading works
1372        let (u2, p2) = read_auth_cookie()?;
1373        assert_eq!(username, u2);
1374        assert_eq!(password, p2);
1375
1376        Ok(())
1377    }
1378
1379    #[test]
1380    fn test_blossom_read_servers_include_write_only_servers_as_fresh_fallbacks() {
1381        let mut config = BlossomConfig::default();
1382        config.servers = vec!["https://legacy.server".to_string()];
1383
1384        let read = config.all_read_servers();
1385        assert!(read.contains(&"https://legacy.server".to_string()));
1386        assert!(read.contains(&"https://cdn.iris.to".to_string()));
1387        assert!(read.contains(&"https://blossom.primal.net".to_string()));
1388        assert!(read.contains(&"https://upload.iris.to".to_string()));
1389
1390        let write = config.all_write_servers();
1391        assert!(write.contains(&"https://legacy.server".to_string()));
1392        assert!(write.contains(&"https://upload.iris.to".to_string()));
1393    }
1394
1395    #[test]
1396    fn test_blossom_servers_fall_back_to_defaults_when_explicitly_empty() {
1397        let config = BlossomConfig {
1398            enabled: true,
1399            servers: Vec::new(),
1400            read_servers: Vec::new(),
1401            write_servers: Vec::new(),
1402            max_upload_mb: default_max_upload_mb(),
1403            require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1404            optimistic_uploads: default_optimistic_uploads(),
1405            replicate_servers: Vec::new(),
1406            replicate_queue_mb: default_replicate_queue_mb(),
1407        };
1408
1409        let read = config.all_read_servers();
1410        let mut expected = default_read_servers();
1411        expected.extend(default_write_servers());
1412        expected.sort();
1413        expected.dedup();
1414        assert_eq!(read, expected);
1415
1416        let write = config.all_write_servers();
1417        assert_eq!(write, default_write_servers());
1418    }
1419
1420    #[test]
1421    fn test_disabled_sources_preserve_lists_but_return_no_active_endpoints() {
1422        let nostr = NostrConfig {
1423            enabled: false,
1424            relays: vec!["wss://relay.example".to_string()],
1425            ..NostrConfig::default()
1426        };
1427        assert!(nostr.active_relays().is_empty());
1428
1429        let blossom = BlossomConfig {
1430            enabled: false,
1431            servers: vec!["https://legacy.server".to_string()],
1432            read_servers: vec!["https://read.example".to_string()],
1433            write_servers: vec!["https://write.example".to_string()],
1434            max_upload_mb: default_max_upload_mb(),
1435            require_random_untrusted_ingest: default_require_random_untrusted_ingest(),
1436            optimistic_uploads: default_optimistic_uploads(),
1437            replicate_servers: Vec::new(),
1438            replicate_queue_mb: default_replicate_queue_mb(),
1439        };
1440        assert!(blossom.all_read_servers().is_empty());
1441        assert!(blossom.all_write_servers().is_empty());
1442    }
1443
1444    #[test]
1445    fn server_defaults_enable_fips_udp_and_webrtc() {
1446        let server = ServerConfig::default();
1447
1448        assert!(server.enable_fips);
1449        assert!(server.enable_fips_udp);
1450        assert!(server.fips_udp_bind_addr.is_none());
1451        assert!(!server.fips_udp_public);
1452        assert!(server.fips_udp_external_addr.is_none());
1453        assert!(server.enable_fips_webrtc);
1454        assert!(server.fetch_from_fips_peers);
1455        assert!(server.fips_relays.is_empty());
1456        assert!(server.fips_peers.is_empty());
1457        assert_eq!(server.fips_discovery_scope, "hashtree-v1");
1458        assert_eq!(server.fips_request_timeout_ms, 5_500);
1459    }
1460
1461    #[test]
1462    fn server_config_reads_fips_overrides() {
1463        let config: Config = toml::from_str(
1464            r#"
1465[server]
1466enable_fips = true
1467fips_discovery_scope = "test-hashtree"
1468fips_relays = ["wss://fips.example"]
1469fips_peers = [
1470  { npub = "npub1origin", udp_addresses = ["udp:192.0.2.10:2121"] },
1471  { npub = "npub1cache" },
1472]
1473enable_fips_udp = false
1474fips_udp_bind_addr = "0.0.0.0:2121"
1475fips_udp_public = true
1476fips_udp_external_addr = "198.19.77.10:2121"
1477enable_fips_webrtc = true
1478fetch_from_fips_peers = false
1479fips_request_timeout_ms = 42
1480"#,
1481        )
1482        .unwrap();
1483
1484        assert!(config.server.enable_fips);
1485        assert_eq!(config.server.fips_discovery_scope, "test-hashtree");
1486        assert_eq!(config.server.fips_relays, ["wss://fips.example"]);
1487        assert_eq!(
1488            config.server.fips_peers,
1489            [
1490                ConfiguredFipsPeer {
1491                    npub: "npub1origin".to_string(),
1492                    udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
1493                },
1494                ConfiguredFipsPeer {
1495                    npub: "npub1cache".to_string(),
1496                    udp_addresses: Vec::new(),
1497                },
1498            ]
1499        );
1500        assert!(!config.server.enable_fips_udp);
1501        assert_eq!(
1502            config.server.fips_udp_bind_addr.as_deref(),
1503            Some("0.0.0.0:2121")
1504        );
1505        assert!(config.server.fips_udp_public);
1506        assert_eq!(
1507            config.server.fips_udp_external_addr.as_deref(),
1508            Some("198.19.77.10:2121")
1509        );
1510        assert!(config.server.enable_fips_webrtc);
1511        assert!(!config.server.fetch_from_fips_peers);
1512        assert_eq!(config.server.fips_request_timeout_ms, 42);
1513    }
1514
1515    #[test]
1516    fn server_config_accepts_legacy_http_fips_fetch_name() {
1517        let config: Config = toml::from_str(
1518            r#"
1519[server]
1520http_fips_fetch = false
1521"#,
1522        )
1523        .unwrap();
1524
1525        assert!(!config.server.fetch_from_fips_peers);
1526    }
1527
1528    #[test]
1529    fn fips_relay_resolution_prefers_fips_relays_then_nostr() {
1530        let active_nostr = vec!["wss://nostr.example".to_string()];
1531        let mut server = ServerConfig::default();
1532
1533        assert_eq!(
1534            server.resolved_fips_relays(&active_nostr),
1535            [
1536                "wss://nostr.example",
1537                "wss://temp.iris.to",
1538                "wss://relay.primal.net"
1539            ]
1540        );
1541
1542        server.fips_relays = vec!["wss://fips.example".to_string()];
1543        assert_eq!(
1544            server.resolved_fips_relays(&["wss://ignored.example".to_string()]),
1545            [
1546                "wss://fips.example",
1547                "wss://temp.iris.to",
1548                "wss://relay.primal.net"
1549            ]
1550        );
1551    }
1552
1553    #[test]
1554    fn fips_relay_resolution_dedupes_bootstrap_relays() {
1555        let mut server = ServerConfig::default();
1556        server.fips_relays = vec![
1557            "wss://temp.iris.to/".to_string(),
1558            " wss://relay.primal.net ".to_string(),
1559            "wss://extra.example".to_string(),
1560        ];
1561
1562        assert_eq!(
1563            server.resolved_fips_relays(&[]),
1564            [
1565                "wss://temp.iris.to",
1566                "wss://relay.primal.net",
1567                "wss://extra.example"
1568            ]
1569        );
1570    }
1571}