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