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