Skip to main content

haste_server/
config.rs

1use derivative::Derivative;
2use haste_fhir_search::config::SearchConfig;
3use haste_repository::config::RepoConfig;
4use haste_worker::async_jobs::AsyncJobWorkerConfig;
5use serde::{Deserialize, Serialize};
6use std::{path::PathBuf, time::Duration};
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
9#[serde(default)]
10pub struct ServerConfig {
11    /// Port on which the server listens.
12    pub port: u16,
13    /// Whether artifact mutations are allowed. If `false`, any attempt to
14    /// create, update, or delete artifacts will be rejected.
15    pub allow_artifact_mutations: bool,
16    /// Used for JWT signing/verification.
17    pub certification_dir: PathBuf,
18    /// Main root where the FHIR server is hosted.
19    pub api_uri: String,
20    /// Where to redirect for the hardcoded admin app.
21    pub admin_app_redirect_uri: String,
22
23    pub fhir: FHIRConfig,
24    pub repo: RepoConfig,
25    pub search: SearchConfig,
26    pub email: Option<EmailConfig>,
27    pub rate_limits: RateLimitsConfig,
28    pub max_request_body_size: usize,
29    pub monitoring: MonitoringConfig,
30    pub security: SecurityConfig,
31    pub operations: OperationsConfig,
32    pub signup: SignupConfig,
33    pub jobs: JobsConfig,
34    pub packages: PackagesConfig,
35}
36
37/// Asynchronous jobs: an operation invoked with `Prefer: respond-async` today,
38/// Bulk Data exports next. The first three apply to the server's kick-off and
39/// status endpoints; the rest to the worker that runs the jobs.
40#[derive(Debug, Clone, Deserialize, Serialize)]
41#[serde(default)]
42pub struct JobsConfig {
43    /// How long a finished job and its result stay available to poll, in
44    /// seconds, and how long a job that never ran waits before it is cleared.
45    pub retention_seconds: u64,
46    /// Queued or running jobs one project may have at once. A kick-off past
47    /// this is refused with `429 Too Many Requests`, as the Bulk Data
48    /// specification allows.
49    pub max_live_per_project: i64,
50    /// What a polling client is told to wait, in seconds (`Retry-After`),
51    /// while its job is still queued or running.
52    pub retry_after_seconds: u64,
53    /// How long one claim or lease renewal holds a job for a worker, in
54    /// seconds. A worker that stops renewing for this long loses the job.
55    pub lease_seconds: u64,
56    /// How often an idle worker looks for queued jobs, in milliseconds.
57    pub poll_interval_ms: u64,
58    /// Jobs one worker runs at once.
59    pub max_concurrent: usize,
60    /// How often a worker sweeps abandoned and expired jobs, in seconds.
61    pub maintenance_interval_seconds: u64,
62}
63
64impl JobsConfig {
65    #[must_use]
66    pub fn retention(&self) -> Duration {
67        Duration::from_secs(self.retention_seconds)
68    }
69
70    #[must_use]
71    pub fn worker_config(&self) -> AsyncJobWorkerConfig {
72        AsyncJobWorkerConfig {
73            poll_interval: Duration::from_millis(self.poll_interval_ms),
74            lease_duration: Duration::from_secs(self.lease_seconds),
75            max_concurrent_jobs: self.max_concurrent,
76            maintenance_interval: Duration::from_secs(self.maintenance_interval_seconds),
77            retention: self.retention(),
78        }
79    }
80}
81
82impl Default for JobsConfig {
83    fn default() -> Self {
84        Self {
85            retention_seconds: 24 * 60 * 60,
86            max_live_per_project: 10,
87            retry_after_seconds: 5,
88            lease_seconds: 60,
89            poll_interval_ms: 1000,
90            max_concurrent: 4,
91            maintenance_interval_seconds: 60,
92        }
93    }
94}
95
96/// Installing FHIR packages (implementation guides) into a project with
97/// `ImplementationGuide/$install`.
98#[derive(Debug, Clone, Deserialize, Serialize)]
99#[serde(default)]
100pub struct PackagesConfig {
101    /// The package registry a package named by `name@version` is fetched
102    /// from. It serves the npm-style API packages.fhir.org does and must be
103    /// an https URL; a redirect is followed only within it. Empty turns off
104    /// fetching, so packages can only be uploaded.
105    pub registry_url: String,
106    /// Packages not installed when another package depends on them: large
107    /// terminology and cross-version packages, that would add thousands of
108    /// resources to every project that installs an IG. Naming one in `$install` still
109    /// installs it. The FHIR core packages are always skipped, as the server
110    /// has them built in.
111    pub skip_dependencies: Vec<String>,
112    /// The largest package tarball fetched or uploaded, compressed, in bytes.
113    pub max_package_bytes: u64,
114    /// The most files one package may hold.
115    pub max_files: usize,
116    /// The most packages one install may fetch or upload, dependencies of
117    /// dependencies included. A fetch that fails still counts.
118    pub max_install_packages: usize,
119    /// The most the packages of one install may unpack to together, uploads
120    /// included, in bytes. It is enforced while unpacking. An install's peak
121    /// memory is about four times what its packages unpack to.
122    pub max_install_bytes: u64,
123    /// Installs this server runs at once; one more is refused with 429. One
124    /// install at the byte limit can peak near 800 MB, so the default is 1.
125    pub max_concurrent_installs: usize,
126    /// How often one Free tier tenant may start an install, in seconds;
127    /// another within this time is refused with 429. 0 turns the wait off.
128    pub free_tier_install_interval_seconds: u64,
129    /// How often any Free tier tenant may start an install, all of them
130    /// together, in seconds; another within this time is refused with 429.
131    /// It keeps many Free tier tenants taking turns from holding the
132    /// install slot, so set it well above how long one install takes.
133    /// 0 turns the wait off.
134    pub free_tier_shared_install_interval_seconds: u64,
135}
136
137impl Default for PackagesConfig {
138    fn default() -> Self {
139        Self {
140            registry_url: "https://packages.fhir.org".to_string(),
141            skip_dependencies: [
142                "hl7.terminology",
143                "hl7.terminology.r4",
144                "hl7.terminology.r5",
145                "us.nlm.vsac",
146                "us.cdc.phinvads",
147                "hl7.fhir.uv.xver-r5.r4",
148                "hl7.fhir.uv.xver-r4b.r4",
149                "hl7.fhir.uv.xver-r3.r4",
150                "hl7.fhir.uv.xver-r2.r4",
151            ]
152            .map(str::to_string)
153            .to_vec(),
154            max_package_bytes: 20 * 1024 * 1024,
155            max_files: 20_000,
156            max_install_packages: 50,
157            max_install_bytes: 200 * 1024 * 1024,
158            max_concurrent_installs: 1,
159            free_tier_install_interval_seconds: 120,
160            free_tier_shared_install_interval_seconds: 60,
161        }
162    }
163}
164
165/// Sign-up and login at `/auth/signup` and `/auth/login`.
166#[derive(Debug, Clone, Deserialize, Serialize)]
167#[serde(default)]
168pub struct SignupConfig {
169    /// Whether a visitor may create a tenant for themselves. If `false`,
170    /// `/auth/signup` is closed and tenants come only from
171    /// `admin tenant create`. Login is unaffected.
172    pub enabled: bool,
173    /// Optional Cloudflare Turnstile bot check on the email form.
174    pub turnstile: Option<TurnstileConfig>,
175}
176
177#[derive(Derivative, Clone, Deserialize, Serialize)]
178#[derivative(Debug)]
179pub struct TurnstileConfig {
180    /// Public key rendered into the widget.
181    pub site_key: String,
182    /// Server-side key used to verify the widget's token.
183    #[derivative(Debug = "ignore")]
184    pub secret_key: String,
185}
186
187#[derive(Debug, Clone, Deserialize, Serialize)]
188#[serde(default)]
189pub struct SecurityConfig {
190    pub publicize_fhir_metadata: bool,
191    pub mfa: MFAConfig,
192    pub encryption: SecretProviderConfig,
193    pub aes_key: Option<String>,
194    pub certification_key: Option<String>,
195}
196
197#[derive(Debug, Clone, Deserialize, Serialize)]
198#[serde(default)]
199pub struct MFAConfig {
200    pub max_credentials_per_user: usize,
201}
202
203#[derive(Debug, Clone, Deserialize, Serialize)]
204#[serde(default)]
205pub struct OperationsConfig {
206    /// Number of dedicated OS threads in the pool that executes custom
207    /// (tenant-authored) FHIR operation scripts in isolated Deno/V8
208    /// sandboxes. Each thread handles one script invocation at a time, so
209    /// this is a hard ceiling on how many custom operations can run
210    /// concurrently. Defaults to roughly half the host's available
211    /// parallelism.
212    pub deno_pool_threads: usize,
213}
214
215#[derive(Debug, Clone, Deserialize, Serialize)]
216#[serde(default)]
217#[derive(Default)]
218pub struct MonitoringConfig {
219    pub audit_enabled: bool,
220    pub ip_source: IpSource,
221}
222
223#[derive(Debug, Clone, Deserialize, Serialize)]
224#[serde(tag = "type", rename_all = "snake_case")]
225pub enum SecretProviderConfig {
226    Environment { prefix: Option<String> },
227    GCP { project_id: String },
228    AWS { region: String },
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
232#[serde(default)]
233pub struct FHIRConfig {
234    /// Max delete limit for type-delete and system-delete operations.
235    pub delete_limit: u64,
236}
237
238#[derive(Derivative, Clone, Deserialize, Serialize)]
239#[derivative(Debug)]
240#[serde(tag = "backend", rename_all = "snake_case")]
241pub enum EmailConfig {
242    SendGrid {
243        #[derivative(Debug = "ignore")]
244        api_key: String,
245        #[derivative(Debug = "ignore")]
246        from_address: String,
247    },
248    /// Logs each message instead of sending it. For local development.
249    Log,
250}
251
252#[derive(Debug, Clone, Deserialize, Serialize)]
253#[serde(default)]
254pub struct RateLimitsConfig {
255    pub rate_limit_subscription_tiers: Option<[usize; 4]>,
256    pub rate_limit_window_seconds: u64,
257    pub rate_limit_operation_points: u32,
258}
259
260#[derive(Debug, Clone, Deserialize, Serialize, Default)]
261#[serde(rename_all = "snake_case")]
262pub enum IpSource {
263    #[default]
264    ConnectInfo,
265    CfConnectingIp,
266    XRealIp,
267}
268
269impl Default for FHIRConfig {
270    fn default() -> Self {
271        Self { delete_limit: 100 }
272    }
273}
274
275impl Default for ServerConfig {
276    fn default() -> Self {
277        Self {
278            port: 3000,
279            allow_artifact_mutations: false,
280            certification_dir: PathBuf::from("certifications"),
281            api_uri: "http://localhost:3000".into(),
282            admin_app_redirect_uri: "http://*.localhost:3001".into(),
283            fhir: FHIRConfig::default(),
284            repo: RepoConfig::default(),
285            search: SearchConfig::default(),
286            email: None,
287            max_request_body_size: 4 * 1024 * 1024,
288            rate_limits: RateLimitsConfig::default(),
289            monitoring: MonitoringConfig::default(),
290            security: SecurityConfig::default(),
291            operations: OperationsConfig::default(),
292            signup: SignupConfig::default(),
293            jobs: JobsConfig::default(),
294            packages: PackagesConfig::default(),
295        }
296    }
297}
298
299impl Default for SignupConfig {
300    fn default() -> Self {
301        Self {
302            enabled: true,
303            turnstile: None,
304        }
305    }
306}
307
308impl Default for RateLimitsConfig {
309    fn default() -> Self {
310        Self {
311            rate_limit_subscription_tiers: None,
312            rate_limit_window_seconds: 60 * 60 * 24, // 1 day in seconds
313            rate_limit_operation_points: 100,
314        }
315    }
316}
317
318impl Default for SecurityConfig {
319    fn default() -> Self {
320        Self {
321            publicize_fhir_metadata: true,
322            mfa: MFAConfig::default(),
323            encryption: SecretProviderConfig::default(),
324            aes_key: None,
325            certification_key: None,
326        }
327    }
328}
329
330impl Default for MFAConfig {
331    fn default() -> Self {
332        Self {
333            max_credentials_per_user: 1,
334        }
335    }
336}
337
338impl Default for OperationsConfig {
339    fn default() -> Self {
340        let available = std::thread::available_parallelism()
341            .map(std::num::NonZeroUsize::get)
342            .unwrap_or(4);
343
344        Self {
345            deno_pool_threads: (available / 2).max(2),
346        }
347    }
348}
349
350impl Default for SecretProviderConfig {
351    fn default() -> Self {
352        Self::Environment {
353            prefix: Some("HASTE_SECRET_".to_string()),
354        }
355    }
356}