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