Skip to main content

nap_core/server/
config.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Lore server configuration generation
4//!
5//! Generates local.toml configuration for Lore server with platform-independent paths
6//! and recommended persistent defaults for NAP-managed deployments.
7
8use anyhow::{Context, Result};
9use rand::RngCore;
10use std::fs;
11use std::io::Write;
12use std::path::Path;
13
14/// Generate Lore server configuration for local deployment
15///
16/// Creates a complete local.toml configuration file with:
17/// - Persistent stores under NAP home directory
18/// - Persistent certificates
19/// - Recommended defaults for local development
20pub fn generate_local_config(nap_home: &Path) -> Result<ConfigFiles> {
21    let lore_config_dir = nap_home.join("lore").join("config");
22    let local_toml_path = lore_config_dir.join("local.toml");
23
24    fs::create_dir_all(&lore_config_dir).context("Failed to create Lore config directory")?;
25
26    // Only regenerate if missing
27    if local_toml_path.exists() {
28        tracing::info!("Lore config already exists at {:?}", local_toml_path);
29        return Ok(ConfigFiles {
30            config_path: local_toml_path,
31            config_dir: lore_config_dir,
32        });
33    }
34
35    tracing::info!("Generating Lore server configuration");
36
37    let config = generate_config_toml(nap_home);
38    let mut options = fs::OpenOptions::new();
39    options.write(true).create_new(true);
40    #[cfg(unix)]
41    {
42        use std::os::unix::fs::OpenOptionsExt;
43        options.mode(0o600);
44    }
45    let mut file = options
46        .open(&local_toml_path)
47        .context("Failed to create Lore configuration file")?;
48    file.write_all(config.as_bytes())
49        .context("Failed to write Lore configuration file")?;
50
51    tracing::info!("Lore configuration generated at {:?}", local_toml_path);
52
53    Ok(ConfigFiles {
54        config_path: local_toml_path,
55        config_dir: lore_config_dir,
56    })
57}
58
59/// Generate the TOML configuration content
60fn generate_config_toml(nap_home: &Path) -> String {
61    let immutable_path = nap_home.join("lore").join("store").join("immutable");
62    let mutable_path = nap_home.join("lore").join("store").join("mutable");
63    let _cert_path = nap_home.join("lore").join("certs").join("cert.pem");
64    let _key_path = nap_home.join("lore").join("certs").join("key.pem");
65    let mut presign_key = [0_u8; 32];
66    rand::rng().fill_bytes(&mut presign_key);
67    let presign_key = hex::encode(presign_key);
68
69    format!(
70        r#"
71# =============================================================================
72# Lore Server Configuration (Generated by NAP SDK)
73# =============================================================================
74# This configuration is automatically generated by the NAP SDK.
75# Manual modifications may be overwritten.
76
77[server]
78connection_close_timeout_seconds = 5
79runtime_shutdown_timeout_seconds = 25
80
81# Public facing QUIC server settings
82[server.quic]
83enabled = true
84host = "0.0.0.0"
85port = 41337
86verify_client_certs = false
87idle_timeout = 30_000
88keep_alive = 500
89max_bidi_streams = 8
90num_listeners = 10
91transport_bits_per_second = 1_073_741_824  # 1 gbit/s
92transport_rtt = 1  # 1 ms for local development (was 100ms)
93handler_timeout_seconds = 50
94
95# gRPC server settings
96[server.grpc]
97enabled = true
98host = "0.0.0.0"
99port = 41337
100request_handler_timeout_seconds = 50
101verify_client_certs = false
102
103# HTTP server settings
104[server.http]
105enabled = true
106host = "0.0.0.0"
107port = 41339
108max_file_size = 10_485_760  # 10MB
109request_timeout_seconds = 300
110request_body_timeout_seconds = 3600
111available_interval_seconds = 30
112available_timeout_seconds = 5
113store_health_check = false
114presigned_url_hmac_key = "{}"
115
116# =============================================================================
117# Store Configuration
118# =============================================================================
119
120[immutable_store]
121mode = "local"
122
123[immutable_store.local]
124path = "{}"
125flush_delay_seconds = 0
126
127[mutable_store]
128mode = "local"
129
130[mutable_store.local]
131path = "{}"
132flush_delay_seconds = 0
133
134[lock_store]
135mode = "local"
136
137# =============================================================================
138# Tokio Runtime Configuration
139# =============================================================================
140
141[tokio]
142max_blocking_threads = 512
143
144# =============================================================================
145# Telemetry Configuration
146# =============================================================================
147
148[telemetry.logger]
149enable_otlp = false
150format = "json"
151output = "stdout"
152
153[telemetry.metrics]
154export_interval_millis = 30000
155sample_interval_millis = 10000
156
157[telemetry.traces]
158sample_rate = 0.05
159sample_rate_low_tier = 0.001
160
161# =============================================================================
162# Notification Configuration
163# =============================================================================
164
165[notification]
166mode = "local"
167
168# =============================================================================
169# Other Features Configuration
170# =============================================================================
171
172[feature]
173history_step_size = 100
174"#,
175        presign_key,
176        immutable_path.display(),
177        mutable_path.display()
178    )
179}
180
181/// Paths to generated configuration files
182#[derive(Debug, Clone)]
183pub struct ConfigFiles {
184    pub config_path: std::path::PathBuf,
185    pub config_dir: std::path::PathBuf,
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use tempfile::TempDir;
192
193    #[test]
194    fn test_generate_local_config() {
195        let temp_dir = TempDir::new().unwrap();
196        let nap_home = temp_dir.path();
197
198        let files = generate_local_config(nap_home).unwrap();
199
200        assert!(files.config_path.exists());
201        assert!(files.config_dir.exists());
202
203        // Verify configuration content
204        let content = fs::read_to_string(&files.config_path).unwrap();
205        assert!(content.contains("[server.quic]"));
206        assert!(content.contains("port = 41337"));
207        assert!(content.contains("[immutable_store.local]"));
208        assert!(content.contains("[mutable_store.local]"));
209        let metadata = fs::metadata(&files.config_path).unwrap();
210        #[cfg(unix)]
211        {
212            use std::os::unix::fs::PermissionsExt;
213            assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
214        }
215
216        // Verify we can regenerate without error (should skip if exists)
217        let files2 = generate_local_config(nap_home).unwrap();
218        assert_eq!(files.config_path, files2.config_path);
219    }
220
221    #[test]
222    fn test_config_toml_content() {
223        let temp_dir = TempDir::new().unwrap();
224        let nap_home = temp_dir.path();
225
226        let config = generate_config_toml(nap_home);
227
228        // Verify key configuration sections
229        assert!(config.contains("[server.quic]"));
230        assert!(config.contains("port = 41337"));
231        assert!(config.contains("[server.grpc]"));
232        assert!(config.contains("[server.http]"));
233        assert!(config.contains("port = 41339"));
234        let key = config
235            .lines()
236            .find_map(|line| {
237                line.strip_prefix("presigned_url_hmac_key = \"")?
238                    .strip_suffix('"')
239            })
240            .unwrap();
241        assert_eq!(key.len(), 64);
242        assert!(key.bytes().all(|byte| byte.is_ascii_hexdigit()));
243        assert!(config.contains("[immutable_store]"));
244        assert!(config.contains("mode = \"local\""));
245        assert!(config.contains("[mutable_store]"));
246        assert!(config.contains("[lock_store]"));
247        assert!(config.contains("[telemetry.logger]"));
248        assert!(config.contains("[telemetry.metrics]"));
249        assert!(config.contains("[notification]"));
250        assert!(config.contains("[feature]"));
251
252        // Verify paths are included
253        assert!(config.contains(&nap_home.display().to_string()));
254    }
255}