Skip to main content

ordinary_config/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![warn(clippy::all, clippy::pedantic)]
4#![allow(clippy::missing_errors_doc)]
5
6// Copyright (C) 2026 Ordinary Labs, LLC.
7//
8// SPDX-License-Identifier: AGPL-3.0-only
9
10mod validate;
11
12use crate::validate::validate;
13
14use anyhow::bail;
15use arrayvec::ArrayVec;
16use hashbrown::HashSet;
17use ordinary_types::{Field, Kind};
18use serde::{Deserialize, Serialize};
19use std::env;
20use std::fmt::{Display, Formatter, Write};
21use std::path::Path;
22use std::process::Command;
23use tracing::instrument;
24
25fn default_env_name() -> String {
26    "development".to_string()
27}
28
29#[derive(Deserialize, Serialize, Clone)]
30pub struct OrdinaryApiConfig {
31    pub domain: String,
32    pub contacts: Vec<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    #[serde(default)]
35    pub public_dns_ip: Option<[u8; 4]>,
36    #[serde(default = "default_env_name")]
37    pub env_name: String,
38    #[serde(default)]
39    pub limits: OrdinaryApiLimits,
40}
41
42#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
43#[derive(Deserialize, Serialize, Debug, Clone)]
44pub struct AssetsLimits {
45    /// Allowed extensions corresponding to
46    /// [MIME](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types) types.
47    ///
48    /// (More to be supported in the future)
49    ///
50    /// Supported:
51    /// - "txt"
52    /// - "xml"
53    /// - "html"
54    /// - "css"
55    /// - "css.map"
56    /// - "csv"
57    /// - "js"
58    /// - "png"
59    /// - "apng"
60    /// - "gif"
61    /// - "svg"
62    /// - "jpg"
63    /// - "jpeg"
64    /// - "bmp"
65    /// - "tif"
66    /// - "tiff"
67    /// - "webp"
68    /// - "avif"
69    /// - "ico"
70    /// - "pdf"
71    /// - "json"
72    /// - "wasm"
73    ///
74    /// Special:
75    /// - "none" (for no extension; uses "application/octet-stream")
76    /// - "any" (for unsupported extensions; also uses "application/octet-stream")
77    pub allowed_extensions: Vec<String>,
78    /// Limit on total storage for all assets within an app
79    /// (bytes).
80    pub max_store_size: u64,
81    /// Limit on individual asset size (bytes).
82    pub max_asset_size: u64,
83}
84
85impl Default for AssetsLimits {
86    fn default() -> Self {
87        Self {
88            allowed_extensions: vec![
89                "otf".into(),
90                "ttf".into(),
91                "woff".into(),
92                "woff2".into(),
93                "txt".into(),
94                "xml".into(),
95                "html".into(),
96                "css".into(),
97                "css.map".into(),
98                "csv".into(),
99                "js".into(),
100                "png".into(),
101                "apng".into(),
102                "gif".into(),
103                "svg".into(),
104                "jpg".into(),
105                "jpeg".into(),
106                "bmp".into(),
107                "tif".into(),
108                "tiff".into(),
109                "webp".into(),
110                "avif".into(),
111                "ico".into(),
112                "pdf".into(),
113                "json".into(),
114                "wasm".into(),
115            ],
116            max_store_size: 100_000_000,
117            max_asset_size: 1_500_000,
118        }
119    }
120}
121
122#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
123#[derive(Deserialize, Serialize, Debug, Clone)]
124pub struct ArtifactLimits {
125    pub max_store_size: u64,
126    pub max_artifact_size: u64,
127}
128
129impl Default for ArtifactLimits {
130    fn default() -> Self {
131        Self {
132            max_store_size: 10_000_000,
133            max_artifact_size: 500_000,
134        }
135    }
136}
137
138#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
139#[derive(Deserialize, Serialize, Debug, Clone)]
140pub struct CacheLimits {
141    pub max_size_range: (u64, u64),
142    pub max_count_range: (usize, usize),
143
144    pub clean_interval_ranges: ((u64, u64), (u64, u64)),
145}
146
147impl Default for CacheLimits {
148    fn default() -> Self {
149        Self {
150            max_size_range: (1_000_000, 10_000_000),
151            max_count_range: (100, 500),
152
153            clean_interval_ranges: ((5, 10), (15, 20)),
154        }
155    }
156}
157
158#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
159#[derive(Deserialize, Serialize, Debug, Clone)]
160pub struct ContentLimits {
161    pub search_enabled: bool,
162
163    pub max_content_definitions: u8,
164    pub max_content_fields: u8,
165
166    pub max_store_size: u64,
167    pub max_object_size: u64,
168    pub max_field_size: u64,
169}
170
171impl Default for ContentLimits {
172    fn default() -> Self {
173        Self {
174            search_enabled: true,
175
176            max_content_definitions: 255,
177            max_content_fields: 255,
178
179            max_store_size: 10_000_000,
180            max_object_size: 400_000,
181            max_field_size: 200_000,
182        }
183    }
184}
185
186#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
187#[derive(Deserialize, Serialize, Debug, Clone)]
188pub struct ModelLimits {
189    pub search_enabled: bool,
190
191    pub max_model_definitions: u8,
192    pub max_model_fields: u8,
193
194    pub max_item_size: u64,
195    pub max_field_size: u64,
196}
197
198impl Default for ModelLimits {
199    fn default() -> Self {
200        Self {
201            search_enabled: true,
202
203            max_model_definitions: 255,
204            max_model_fields: 255,
205
206            max_item_size: 400_000,
207            max_field_size: 100_000,
208        }
209    }
210}
211
212#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
213#[derive(Deserialize, Serialize, Debug, Clone)]
214pub struct SecretsLimits {
215    pub max_count: u8,
216    pub max_size: u64,
217}
218
219impl Default for SecretsLimits {
220    fn default() -> Self {
221        Self {
222            max_count: 225,
223            max_size: 2_000,
224        }
225    }
226}
227
228#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
229#[derive(Deserialize, Serialize, Debug, Clone)]
230pub struct StorageLimits {
231    /// Maximum total storage for all apps and api.
232    ///
233    /// Unit: bytes.
234    ///
235    /// Default: 10 GB
236    pub max_storage: u64,
237    /// Maximum per-app storage.
238    ///
239    /// Unit: bytes.
240    ///
241    /// Default: 50 MB
242    pub max_app_storage: u64,
243
244    pub assets: AssetsLimits,
245    pub artifact: ArtifactLimits,
246    pub cache: CacheLimits,
247    pub content: ContentLimits,
248    pub model: ModelLimits,
249    pub secrets: SecretsLimits,
250}
251
252impl Default for StorageLimits {
253    fn default() -> Self {
254        Self {
255            max_storage: 20_000_000_000,
256            max_app_storage: 50_000_000,
257
258            assets: AssetsLimits::default(),
259            artifact: ArtifactLimits::default(),
260            cache: CacheLimits::default(),
261            content: ContentLimits::default(),
262            model: ModelLimits::default(),
263            secrets: SecretsLimits::default(),
264        }
265    }
266}
267
268#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
269#[derive(Deserialize, Serialize, Debug, Clone, Default)]
270pub struct MonitorLimits {}
271
272#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
273#[derive(Deserialize, Serialize, Debug, Clone)]
274pub struct IntegrationLimits {
275    /// Max number of integrations per app.
276    pub count: u8,
277
278    /// Integration request timeout.
279    ///
280    /// Unit: seconds.
281    pub max_timeout: u16,
282}
283
284impl Default for IntegrationLimits {
285    fn default() -> Self {
286        Self {
287            count: 255,
288            max_timeout: 10,
289        }
290    }
291}
292
293#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
294#[derive(Deserialize, Serialize, Debug, Clone)]
295pub struct ActionLimits {
296    /// Max number of actions per app.
297    pub count: u8,
298
299    /// Action request timeout.
300    ///
301    /// Unit: seconds.
302    pub max_timeout: u16,
303}
304
305impl Default for ActionLimits {
306    fn default() -> Self {
307        Self {
308            count: 255,
309            max_timeout: 10,
310        }
311    }
312}
313
314#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
315#[derive(Deserialize, Serialize, Debug, Clone, Default)]
316pub struct AuthLimits {}
317
318#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
319#[derive(Deserialize, Serialize, Debug, Clone)]
320pub struct TemplateLimits {
321    /// Max number of templates per app.
322    pub count: u8,
323
324    /// Template request timeout.
325    ///
326    /// Unit: seconds.
327    pub max_timeout: u16,
328}
329
330impl Default for TemplateLimits {
331    fn default() -> Self {
332        Self {
333            count: 255,
334            max_timeout: 10,
335        }
336    }
337}
338
339#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
340#[derive(Deserialize, Serialize, Debug, Clone)]
341pub struct OrdinaryApiLimits {
342    pub app_domains: Vec<String>,
343    pub privileged_domains: Vec<String>,
344
345    /// Max default timeout for all HTTP requests.
346    ///
347    /// Default: 10
348    pub max_default_timeout: u16,
349
350    pub action: ActionLimits,
351    pub auth: AuthLimits,
352    pub integration: IntegrationLimits,
353    pub monitor: MonitorLimits,
354    pub storage: StorageLimits,
355    pub template: TemplateLimits,
356}
357
358impl Default for OrdinaryApiLimits {
359    fn default() -> Self {
360        Self {
361            app_domains: vec![],
362            privileged_domains: vec![],
363
364            max_default_timeout: 10,
365
366            action: ActionLimits::default(),
367            auth: AuthLimits::default(),
368            integration: IntegrationLimits::default(),
369            monitor: MonitorLimits::default(),
370            storage: StorageLimits::default(),
371            template: TemplateLimits::default(),
372        }
373    }
374}
375
376#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
377#[derive(Deserialize, Serialize, Debug, Clone)]
378pub struct ClientLoggingConfig {
379    /// bottom end of the delayed delivery range (seconds)
380    #[serde(skip_serializing_if = "Option::is_none")]
381    #[serde(default)]
382    min_delay: Option<u32>,
383    /// top end of delayed delivery range (seconds)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    #[serde(default)]
386    max_delay: Option<u32>,
387    /// max number of events to be buffered on the client
388    /// prior to flush.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    #[serde(default)]
391    max_buffer: Option<u16>,
392    /// sets the max number of events in a given request.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    #[serde(default)]
395    max_batch: Option<u16>,
396}
397
398#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
399#[derive(Deserialize, Serialize, Debug, Clone)]
400pub enum RedactedHashAlg {
401    Blake2,
402    Blake3,
403}
404
405#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
406#[derive(Deserialize, Serialize, Debug, Clone)]
407pub struct ServerLoggingConfig {
408    #[serde(skip_serializing_if = "Option::is_none")]
409    #[serde(default)]
410    pub ips: Option<bool>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    #[serde(default)]
413    pub headers: Option<bool>,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    #[serde(default)]
416    pub credentials: Option<RedactedHashAlg>,
417    #[serde(skip_serializing_if = "Option::is_none")]
418    #[serde(default)]
419    pub timing: Option<bool>,
420    #[serde(skip_serializing_if = "Option::is_none")]
421    #[serde(default)]
422    pub sizes: Option<bool>,
423}
424
425#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
426#[derive(Deserialize, Serialize, Debug, Clone)]
427pub struct LoggingConfig {
428    #[serde(skip_serializing_if = "Option::is_none")]
429    #[serde(default)]
430    pub client: Option<ClientLoggingConfig>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    #[serde(default)]
433    pub server: Option<ServerLoggingConfig>,
434}
435
436#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
437#[derive(Deserialize, Serialize, Debug, Clone)]
438pub struct Check {
439    // todo: what to validate the token against
440    // ?? i.e "token.fields.account is included in list of post.author.followers.accounts"
441}
442
443#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
444#[derive(Deserialize, Serialize, Debug, Clone)]
445pub enum TokenAlgorithm {
446    HmacBlake2b256,
447}
448
449/// Configuration for refresh tokens.
450#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
451#[derive(Deserialize, Serialize, Debug, Clone)]
452pub struct RefreshTokenConfig {
453    /// Algorithm used for verifying the token.
454    pub algorithm: TokenAlgorithm,
455    /// How long a token should be valid for (seconds).
456    pub lifetime: u32,
457    /// how frequently the key should be rotated
458    pub rotation: u32,
459}
460
461impl Default for RefreshTokenConfig {
462    fn default() -> RefreshTokenConfig {
463        RefreshTokenConfig {
464            algorithm: TokenAlgorithm::HmacBlake2b256,
465            lifetime: 60 * 60 * 24 * 7,
466            rotation: 60 * 60 * 24 * 7 * 2,
467        }
468    }
469}
470
471/// Configuration for access tokens.
472#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
473#[derive(Deserialize, Serialize, Debug, Clone)]
474pub struct AccessTokenConfig {
475    /// Algorithm used for verifying the token.
476    pub algorithm: TokenAlgorithm,
477    /// How long a token should be valid for (seconds).
478    pub lifetime: u32,
479    /// how frequently the key should be rotated
480    pub rotation: u32,
481    /// Token claims structuring.
482    ///
483    /// Note: `idx` starts at 1 to create space for system claims (id, domain, and account).
484    pub claims: Vec<Field>,
485}
486
487impl Default for AccessTokenConfig {
488    fn default() -> AccessTokenConfig {
489        AccessTokenConfig {
490            algorithm: TokenAlgorithm::HmacBlake2b256,
491            lifetime: 60 * 60 * 24,
492            rotation: 60 * 60 * 24 * 3,
493            claims: vec![],
494        }
495    }
496}
497
498/// Configuration for client password hashing.
499///
500/// When JavaScript or WASM modes are enabled, passwords are
501/// hashed with the application name, and account, before transit
502/// (or in the case of WASM prior to the Opaque client operations
503/// if Opaque is selected for the `PasswordProtocol`).
504#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
505#[derive(Deserialize, Serialize, Debug, Clone)]
506pub enum ClientPasswordHash {
507    /// Limited by what browsers can support, SHA-256 is
508    /// a good option for a client-side hash to enable
509    /// slightly better password protection when in
510    /// javascript-only mode, without the WASM for Opaque.
511    Sha256,
512}
513
514/// Configuration for password protocol.
515#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
516#[derive(Deserialize, Serialize, Debug, Clone)]
517pub enum PasswordProtocol {
518    /// When WASM is enabled, this will run the client portions
519    /// browser-side. When JavaScript-only, passwords will be hashed
520    /// and then sent to the server where the client portion will
521    /// be done on behalf of the user. In noscript mode, the password
522    /// is sent only protected by TLS, hashed and then the client operations
523    /// are done server side.
524    ///
525    /// If a user later decides to enable JavaScript or WASM, they'll be
526    /// able to opt in to the no-plain-password-sent modes without interruption.
527    Opaque,
528    // todo: SRP and Bcrypt/Argon2 hashing?
529}
530
531/// Configuration for passwords.
532#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
533#[derive(Deserialize, Serialize, Debug, Clone)]
534pub struct PasswordConfig {
535    pub protocol: PasswordProtocol,
536}
537
538impl Default for PasswordConfig {
539    fn default() -> PasswordConfig {
540        PasswordConfig {
541            protocol: PasswordProtocol::Opaque,
542        }
543    }
544}
545
546#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
547#[derive(Deserialize, Serialize, Debug, Clone)]
548pub enum TotpAlgorithm {
549    /// only allowing SHA1 for now
550    /// because many of the major MFA authenticator
551    /// apps don't support SHA256 or SHA512, and fail silently.
552    Sha1,
553}
554
555#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
556#[derive(Deserialize, Serialize, Debug, Clone)]
557pub struct TotpConfig {
558    /// Used for response after registration form is submitted.
559    /// Will just return a QR code SVG if not set.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    #[serde(default)]
562    pub template: Option<String>,
563    pub algorithm: TotpAlgorithm,
564}
565
566impl Default for TotpConfig {
567    fn default() -> TotpConfig {
568        TotpConfig {
569            template: None,
570            algorithm: TotpAlgorithm::Sha1,
571        }
572    }
573}
574
575#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
576#[derive(Serialize, Deserialize, Debug, Clone, Default)]
577pub struct MfaConfig {
578    pub totp: TotpConfig,
579}
580
581#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
582#[derive(Deserialize, Serialize, Debug, Clone)]
583pub enum InviteMode {
584    /// only the root user can invite
585    Root,
586    /// only a site admin can invite
587    Admin,
588    /// anyone who has been invited can invite anyone else
589    Viral,
590    // ?? anyone who has been invited has a limited set of invites they
591    // ?? can share on an optional interval.
592    // todo: Limited,
593    // ?? only those invited with permission to invite can invite others
594    // todo: Selective,
595}
596
597#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
598#[derive(Deserialize, Serialize, Debug, Clone)]
599pub struct InviteConfig {
600    pub mode: InviteMode,
601    /// How long a token is valid for.
602    ///
603    /// Defaults to 7 days.
604    pub lifetime: u32,
605    /// On what interval to clean up expired token ids.
606    ///
607    /// Defaults to 30 - 90 seconds.
608    pub clean_interval: (u32, u32),
609    /// Values that can be used internally in the API server to
610    /// set default permissions for new accounts.
611    ///
612    /// TODO: in the future, these can also be set in order to pre-validate
613    /// TODO: or constrain `app` account registrations. This will require the
614    /// TODO: use of an `InviteCreate` action trigger (for validating and setting
615    /// TODO: the invite token claims), and a pre-registration `InviteValidate`,
616    /// TODO: as well as passing the invite token claims to the `Registration` trigger
617    /// TODO: (allowing for the validated invite claims to be used in the account
618    /// TODO: claims setting operation).
619    ///
620    /// Note: `idx` starts at 1 to create space for system claims (id, domain, and account).
621    #[serde(skip_serializing_if = "Option::is_none")]
622    #[serde(default)]
623    pub claims: Option<Vec<Field>>,
624}
625
626impl Default for InviteConfig {
627    fn default() -> InviteConfig {
628        InviteConfig {
629            mode: InviteMode::Viral,
630            lifetime: 60 * 60 * 24 * 7,
631            clean_interval: (30, 90),
632            claims: None,
633        }
634    }
635}
636
637/// Auth configuration.
638#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
639#[derive(Deserialize, Serialize, Debug, Clone)]
640pub struct AuthConfig {
641    /// Configuration for passwords.
642    pub password: PasswordConfig,
643    /// MFA configuration for auth.
644    pub mfa: MfaConfig,
645    /// Configuration for refresh tokens.
646    pub refresh_token: RefreshTokenConfig,
647    /// Configuration for access tokens.
648    pub access_token: AccessTokenConfig,
649    /// Determines whether cookies should be used
650    /// for browser based template navigation, and form submissions
651    ///
652    /// Note: when set to `false`, `protected` templates, and actions triggered
653    /// by form submission will fail. Form based registration and login will
654    /// necessarily be disabled, also.
655    ///
656    /// !! Important: when set to `true`, tokens retrieved for `js` and `noscript` flavors
657    /// !! do not support client signatures, because http-only cookies cannot be signed by
658    /// !! the client.
659    pub cookies_enabled: bool,
660    /// what algorithm is used to hash passwords and MFA codes
661    pub client_hash: ClientPasswordHash,
662
663    /// invite config
664    #[serde(skip_serializing_if = "Option::is_none")]
665    #[serde(default)]
666    pub invite: Option<InviteConfig>,
667}
668
669impl Default for AuthConfig {
670    fn default() -> AuthConfig {
671        AuthConfig {
672            password: PasswordConfig::default(),
673            mfa: MfaConfig::default(),
674            refresh_token: RefreshTokenConfig::default(),
675            access_token: AccessTokenConfig::default(),
676            cookies_enabled: false,
677            client_hash: ClientPasswordHash::Sha256,
678            invite: None,
679        }
680    }
681}
682
683/// Compression algorithms
684#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
685#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
686pub enum CompressionAlgorithm {
687    All,
688    Gzip,
689    Zstd { level: u8 },
690    Brotli,
691    Deflate,
692}
693
694impl CompressionAlgorithm {
695    #[must_use]
696    pub fn as_u8(&self) -> u8 {
697        match self {
698            Self::All => 0,
699            Self::Gzip => 1,
700            Self::Zstd { level: _ } => 2,
701            Self::Brotli => 3,
702            Self::Deflate => 4,
703        }
704    }
705
706    #[must_use]
707    pub fn as_char(&self) -> char {
708        match self {
709            Self::All => '0',
710            Self::Gzip => '1',
711            Self::Zstd { level: _ } => '2',
712            Self::Brotli => '3',
713            Self::Deflate => '4',
714        }
715    }
716
717    #[must_use]
718    pub fn as_str(&self) -> &'static str {
719        match self {
720            Self::All => "",
721            Self::Gzip => "gzip",
722            Self::Zstd { level: _ } => "zstd",
723            Self::Brotli => "br",
724            Self::Deflate => "deflate",
725        }
726    }
727}
728
729#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
730#[derive(Deserialize, Serialize, Debug, Clone)]
731pub enum StoredCachePolicy {
732    /// No eviction on clean or write. Constrained only by overall storage limit or `max_size`
733    /// (if set). Will only be evicted if dependencies change and `evict_on_dependency_change`
734    /// is set.
735    Permanent,
736    /// Prioritize the most Frequently accessed, Recently accessed and smallest Sized items
737    ///
738    /// (`frequency_equality_threshold` (hit count), `recency_equality_threshold` (seconds))
739    FRs(u64, u64),
740}
741
742/// Render caching policy.
743///
744/// IMPORTANT: Very experimental, may not work as described. `policy: Permanent` is currently
745/// the most likely to behave correctly.
746#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
747#[derive(Deserialize, Serialize, Debug, Clone)]
748pub struct StoredCache {
749    pub policy: StoredCachePolicy,
750
751    /// Which compression formats should be stored
752    #[serde(skip_serializing_if = "Option::is_none")]
753    #[serde(default)]
754    pub compression: Option<CompressionAlgorithms>,
755
756    #[serde(skip_serializing)]
757    #[serde(default)]
758    pub internal_compression: Option<ArrayVec<CompressionAlgorithm, 4>>,
759
760    /// Upper limit on total time a cached item can be stored
761    ///
762    /// Unit: seconds
763    #[serde(skip_serializing_if = "Option::is_none")]
764    #[serde(default)]
765    pub max_ttl: Option<u64>,
766
767    /// Compared with time since last hit.
768    ///
769    /// Unit: seconds
770    #[serde(skip_serializing_if = "Option::is_none")]
771    #[serde(default)]
772    pub hit_ttl: Option<u64>,
773
774    /// Upper limit on the cumulative size of all cached responses
775    /// for a given template.
776    ///
777    /// Unit: bytes
778    #[serde(skip_serializing_if = "Option::is_none")]
779    #[serde(default)]
780    pub max_size: Option<u64>,
781
782    /// Upper limit on the number of cached responses stored at
783    /// a given time, for a given template.
784    #[serde(skip_serializing_if = "Option::is_none")]
785    #[serde(default)]
786    pub max_count: Option<usize>,
787
788    /// How long an LFU "hit" tick is valid for
789    ///
790    /// Unit: seconds
791    #[serde(skip_serializing_if = "Option::is_none")]
792    #[serde(default)]
793    pub frequency_window: Option<u64>,
794
795    /// Rate at which the cache is cleaned based on other rules.
796    ///
797    /// Option<(min, max)>
798    #[serde(skip_serializing_if = "Option::is_none")]
799    #[serde(default)]
800    pub clean_interval: Option<(u64, u64)>,
801
802    /// Whether a cached item should also track the of models and content
803    /// which it depends on, and evict when they are modified.
804    #[serde(skip_serializing_if = "Option::is_none")]
805    #[serde(default)]
806    pub evict_on_dependency_change: Option<bool>,
807}
808
809#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
810#[derive(Deserialize, Serialize, Debug, Clone)]
811pub enum XXH3Variation {
812    Bit64,
813    Bit128,
814}
815
816/// Hashing algorithm options for generating etags.
817///
818/// `AHash` is the default if none is selected.
819#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
820#[derive(Deserialize, Serialize, Debug, Clone)]
821pub enum HttpEtagAlgorithm {
822    AHash,
823    XXH3(XXH3Variation),
824    Rustc,
825    Blake3,
826}
827
828/// HTTP Cache-Control.
829/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
830#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
831#[derive(Deserialize, Serialize, Debug, Clone)]
832pub struct HttpEtag {
833    #[serde(skip_serializing_if = "Option::is_none")]
834    #[serde(default)]
835    pub alg: Option<HttpEtagAlgorithm>,
836}
837
838/// HTTP Cache-Control.
839/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
840#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
841#[derive(Deserialize, Serialize, Debug, Clone, Default)]
842pub struct HttpCacheControl {
843    #[serde(skip_serializing_if = "Option::is_none")]
844    #[serde(default)]
845    pub max_age: Option<usize>,
846    #[serde(skip_serializing_if = "Option::is_none")]
847    #[serde(default)]
848    pub s_maxage: Option<usize>,
849    #[serde(skip_serializing_if = "Option::is_none")]
850    #[serde(default)]
851    pub no_cache: Option<bool>,
852    #[serde(skip_serializing_if = "Option::is_none")]
853    #[serde(default)]
854    pub no_store: Option<bool>,
855    #[serde(skip_serializing_if = "Option::is_none")]
856    #[serde(default)]
857    pub no_transform: Option<bool>,
858    #[serde(skip_serializing_if = "Option::is_none")]
859    #[serde(default)]
860    pub must_revalidate: Option<bool>,
861    #[serde(skip_serializing_if = "Option::is_none")]
862    #[serde(default)]
863    pub proxy_revalidate: Option<bool>,
864    #[serde(skip_serializing_if = "Option::is_none")]
865    #[serde(default)]
866    pub private: Option<bool>,
867    #[serde(skip_serializing_if = "Option::is_none")]
868    #[serde(default)]
869    pub public: Option<bool>,
870    #[serde(skip_serializing_if = "Option::is_none")]
871    #[serde(default)]
872    pub immutable: Option<bool>,
873    #[serde(skip_serializing_if = "Option::is_none")]
874    #[serde(default)]
875    pub stale_while_revalidate: Option<bool>,
876    #[serde(skip_serializing_if = "Option::is_none")]
877    #[serde(default)]
878    pub stale_if_error: Option<bool>,
879}
880
881impl HttpCacheControl {
882    pub fn header_value(&self, cache_control: &mut String, default: &str) -> anyhow::Result<()> {
883        if let Some(max_age) = self.max_age {
884            write!(cache_control, "max-age={max_age}, ")?;
885        }
886        if let Some(s_maxage) = self.s_maxage {
887            write!(cache_control, "s-maxage={s_maxage}, ")?;
888        }
889        if let Some(no_cache) = self.no_cache
890            && no_cache
891        {
892            write!(cache_control, "no-cache, ")?;
893        }
894        if let Some(no_store) = self.no_store
895            && no_store
896        {
897            write!(cache_control, "no-store, ")?;
898        }
899        if let Some(no_transform) = self.no_transform
900            && no_transform
901        {
902            write!(cache_control, "no-transform, ")?;
903        }
904        if let Some(must_revalidate) = self.must_revalidate
905            && must_revalidate
906        {
907            write!(cache_control, "must-revalidate, ")?;
908        }
909        if let Some(proxy_revalidate) = self.proxy_revalidate
910            && proxy_revalidate
911        {
912            write!(cache_control, "proxy-revalidate, ")?;
913        }
914        if let Some(stale_while_revalidate) = self.stale_while_revalidate
915            && stale_while_revalidate
916        {
917            write!(cache_control, "stale-while-revalidate, ")?;
918        }
919        if let Some(private) = self.private
920            && private
921        {
922            write!(cache_control, "private, ")?;
923        }
924        if let Some(public) = self.public
925            && public
926        {
927            write!(cache_control, "public, ")?;
928        }
929        if let Some(immutable) = self.immutable
930            && immutable
931        {
932            write!(cache_control, "immutable, ")?;
933        }
934        if let Some(stale_if_error) = self.stale_if_error
935            && stale_if_error
936        {
937            write!(cache_control, "stale-if-error, ")?;
938        }
939
940        if cache_control.is_empty() {
941            cache_control.push_str(default);
942        } else {
943            cache_control.pop();
944            cache_control.pop();
945        }
946
947        Ok(())
948    }
949}
950
951#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
952#[derive(Deserialize, Serialize, Debug, Clone, Default)]
953pub struct HttpCache {
954    #[serde(skip_serializing_if = "Option::is_none")]
955    #[serde(default)]
956    pub cache_control: Option<HttpCacheControl>,
957
958    /// Corresponds to the HTTP Expires header.
959    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Expires>
960    ///
961    /// Unit: seconds.
962    #[serde(skip_serializing_if = "Option::is_none")]
963    #[serde(default)]
964    pub expires: Option<u64>,
965
966    #[serde(skip_serializing_if = "Option::is_none")]
967    #[serde(default)]
968    pub etag: Option<HttpEtag>,
969}
970
971/// Field used within the scope of a single template.
972#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
973#[derive(Deserialize, Serialize, Debug, Clone)]
974pub struct TemplateField {
975    /// Field name
976    pub name: String,
977    /// Specifies the type of the value.
978    pub kind: Kind,
979    /// JSON value for template field.
980    pub value: serde_json::Value,
981}
982
983/// Query expression to be used in template bindings.
984#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
985#[derive(Deserialize, Serialize, Debug, Clone)]
986pub enum QueryExpression {
987    Gte,
988    Gt,
989    Lte,
990    Lt,
991    Eq,
992    BeginsWith,
993}
994
995/// Binding options for template field refs.
996#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
997#[derive(Deserialize, Serialize, Debug, Clone)]
998pub enum TemplateRefFieldBind {
999    /// Bind this property to a token field.
1000    Token {
1001        /// Name of the field that this property should bind to.
1002        /// (i.e "account" if you'd like to have an "/account" route
1003        /// that interprets the request based on the logged-in user's
1004        /// token claims/fields).
1005        field: String,
1006        /// Include if the parent field is queryable.
1007        #[serde(skip_serializing_if = "Option::is_none")]
1008        #[serde(default)]
1009        expression: Option<QueryExpression>,
1010    },
1011    /// Bind this property to a route segment (i.e /{something})
1012    Segment {
1013        /// Specifies the name of the route segment that this property is binding to.
1014        name: String,
1015        /// Include if the parent field is queryable.
1016        #[serde(skip_serializing_if = "Option::is_none")]
1017        #[serde(default)]
1018        expression: Option<QueryExpression>,
1019    },
1020}
1021
1022/// Declaration for which fields from a content definition
1023/// or data model to include.
1024#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1025#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1026pub struct TemplateRefField {
1027    /// Specifies index for reference field. Index
1028    /// must be unique across fields for a given reference.
1029    pub idx: u8,
1030    /// Name of field to be included.
1031    pub name: String,
1032    /// Option to bind this field's value to a route
1033    /// segment, querystring parameter or token field.
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    #[serde(default)]
1036    pub bind: Option<TemplateRefFieldBind>,
1037    /// List of any nested subfields to include for this field.
1038    #[cfg_attr(feature = "utoipa", schema(no_recursion))]
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    #[serde(default)]
1041    pub fields: Option<Vec<TemplateRefField>>,
1042}
1043
1044/// How templates reference Content Definitions
1045/// and Data Models, and the specific fields it
1046/// wants to include.
1047#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1048#[derive(Deserialize, Serialize, Debug, Clone)]
1049pub struct TemplateRef {
1050    /// Specifies the index position for the referenced
1051    /// data. Index must be unique across flags, params, data models and
1052    /// content definitions.
1053    pub idx: u8,
1054    /// Name of the model or content definition.
1055    pub name: String,
1056    /// Which fields to include.
1057    pub fields: Vec<TemplateRefField>,
1058    /// for requesting multiple top-level items
1059    ///
1060    /// note: only supported for content. models use relationships.
1061    #[serde(skip_serializing_if = "Option::is_none")]
1062    #[serde(default)]
1063    pub all: Option<String>,
1064}
1065
1066/// Server flags can only be used in templates whose cache is
1067/// set to "Never".
1068#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1069#[derive(Deserialize, Serialize, Debug, Clone)]
1070pub struct TemplateFlagRef {
1071    /// Specifies the index position for the referenced
1072    /// data. Index must be unique across flags, data models and
1073    /// content definitions.
1074    pub idx: u8,
1075    /// Name of the flag.
1076    pub name: String,
1077}
1078
1079#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1080#[derive(Deserialize, Serialize, Debug, Clone)]
1081pub struct TemplateParamRef {
1082    /// Specifies the index position for the referenced
1083    /// data. Index must be unique across flags, data models, params, and
1084    /// content definitions.
1085    pub idx: u8,
1086    /// Name of the param.
1087    pub name: String,
1088}
1089
1090#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1091#[derive(Deserialize, Serialize, Debug, Clone)]
1092pub struct TemplateCache {
1093    #[serde(skip_serializing_if = "Option::is_none")]
1094    #[serde(default)]
1095    pub stored: Option<StoredCache>,
1096
1097    #[serde(skip_serializing_if = "Option::is_none")]
1098    #[serde(default)]
1099    pub http: Option<HttpCache>,
1100}
1101
1102/// Corresponds to <https://docs.rs/wasm-opt/latest/wasm_opt/struct.OptimizationOptions.html#impl-OptimizationOptions>.
1103///
1104/// Certain levels may not work with `wasmtime`/`cranelift`.
1105#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1106#[derive(Deserialize, Serialize, Debug, Clone)]
1107pub enum WasmOpt {
1108    Size,
1109    SizeAggressive,
1110    Level0,
1111    Level1,
1112    Level2,
1113    Level3,
1114    Level4,
1115}
1116
1117impl WasmOpt {
1118    #[must_use]
1119    pub fn as_flag(&self) -> &'static str {
1120        match self {
1121            Self::Size => "-Os",
1122            Self::SizeAggressive => "-Oz",
1123            Self::Level0 => "-O0",
1124            Self::Level1 => "-O1",
1125            Self::Level2 => "-O2",
1126            Self::Level3 => "-O3",
1127            Self::Level4 => "-O4",
1128        }
1129    }
1130}
1131
1132#[allow(clippy::derivable_impls)]
1133impl Default for TemplateFfiVersion {
1134    fn default() -> Self {
1135        Self::V1
1136    }
1137}
1138
1139#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1140#[derive(Deserialize, Serialize, Debug, Clone)]
1141pub enum TemplateFfiVersion {
1142    V1,
1143}
1144
1145#[allow(clippy::derivable_impls)]
1146impl Default for TemplateFfiSerialization {
1147    fn default() -> Self {
1148        Self::FlexBufferVector
1149    }
1150}
1151
1152/// Input serialization format. Output is always an array of bytes.
1153#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1154#[derive(Deserialize, Serialize, Debug, Clone)]
1155pub enum TemplateFfiSerialization {
1156    FlexBufferVector,
1157}
1158
1159#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1160#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1161pub struct TemplateFfi {
1162    pub version: TemplateFfiVersion,
1163    pub serialization: TemplateFfiSerialization,
1164}
1165
1166/// Template configuration for Ordinary Applications.
1167#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1168#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1169pub struct TemplateConfig {
1170    /// Foreign function interface config
1171    pub ffi: TemplateFfi,
1172
1173    /// Unique index for template.
1174    pub idx: u8,
1175    /// Template's name.
1176    pub name: String,
1177    /// Used as the `content-type` header for HTTP responses.
1178    /// Validated against file extension (if `path` is present).
1179    // todo: switch this to an enum of mime types
1180    pub mime: String,
1181    /// Specifies whether the content in the file should
1182    /// be "minified"/have whitespace removed.
1183    #[serde(skip_serializing_if = "Option::is_none")]
1184    #[serde(default)]
1185    pub minify: Option<bool>,
1186    /// Relative path to the template file
1187    #[serde(skip_serializing_if = "Option::is_none")]
1188    #[serde(default)]
1189    pub path: Option<String>,
1190    /// The route used in the HTTP server to serve this template.
1191    /// Can use segments to bind to properties on models or content
1192    /// definitions:
1193    ///
1194    /// ```json
1195    /// {
1196    ///     ...
1197    ///     "route": "/posts/{slug}",
1198    ///     ...
1199    ///     "models": [
1200    ///         {
1201    ///             "name": "post",
1202    ///             "fields": [
1203    ///                 { "name": "slug", "bind": { "Segment": { "name": "slug" } } },
1204    ///             ]
1205    ///         }
1206    ///     ]
1207    /// }
1208    /// ```
1209    pub route: String,
1210    /// What to check the token fields against. If left blank, route is considered public.
1211    #[serde(skip_serializing_if = "Option::is_none")]
1212    #[serde(default)]
1213    pub protected: Option<Check>,
1214    /// Used to specify the cache policy for this template.
1215    #[serde(skip_serializing_if = "Option::is_none")]
1216    #[serde(default)]
1217    pub cache: Option<TemplateCache>,
1218
1219    /// HTTP Content Security Policy configuration.
1220    ///
1221    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>
1222    ///
1223    /// "Base" defaults to `default-src 'self';` and tacks on SHA-256 integrity
1224    /// hashes for all inlined scripts and styles (generated at build time) to
1225    /// `script-src 'self' sha256-b64` and `style-src 'self' sha256-b64`, respectively.
1226    ///
1227    /// `https:` is used when not running in `--insecure` mode.
1228    #[serde(skip_serializing_if = "Option::is_none")]
1229    #[serde(default)]
1230    pub csp: Option<HttpCsp>,
1231
1232    #[serde(skip_serializing_if = "Option::is_none")]
1233    #[serde(default)]
1234    pub cors: Option<HttpCors>,
1235
1236    /// Max duration for the template.
1237    ///
1238    /// Unit: seconds
1239    #[serde(skip_serializing_if = "Option::is_none")]
1240    #[serde(default)]
1241    pub timeout: Option<u16>,
1242
1243    /// Used for template-specific variables that don't need to be shared
1244    /// beyond the scope of the given template, and don't warrant a content
1245    /// object.
1246    #[serde(skip_serializing_if = "Option::is_none")]
1247    #[serde(default)]
1248    pub fields: Option<Vec<TemplateField>>,
1249    /// List of global variables to be included with the compiled template
1250    /// binary. Globals are excluded by default and have to be explicitly
1251    /// listed in the globals to be accessed from the template.
1252    #[serde(skip_serializing_if = "Option::is_none")]
1253    #[serde(default)]
1254    pub globals: Option<Vec<String>>,
1255    /// List of flags to be referenced by the template.
1256    #[serde(skip_serializing_if = "Option::is_none")]
1257    #[serde(default)]
1258    pub flags: Option<Vec<TemplateFlagRef>>,
1259    /// List of params to be referenced by the template.
1260    #[serde(skip_serializing_if = "Option::is_none")]
1261    #[serde(default)]
1262    pub params: Option<Vec<TemplateParamRef>>,
1263    /// List of models and what fields the template needs from the models.
1264    /// This is effectively a query definition.
1265    #[serde(skip_serializing_if = "Option::is_none")]
1266    #[serde(default)]
1267    pub models: Option<Vec<TemplateRef>>,
1268    /// List of content definitions and the content definition fields
1269    /// that this template will use.
1270    #[serde(skip_serializing_if = "Option::is_none")]
1271    #[serde(default)]
1272    pub content: Option<Vec<TemplateRef>>,
1273
1274    /// Specifies which actions this template triggers, and adds
1275    /// this template's route pattern to the action's list of valid
1276    /// origins.
1277    #[serde(skip_serializing_if = "Option::is_none")]
1278    #[serde(default)]
1279    pub actions: Option<Vec<String>>,
1280
1281    #[serde(skip_serializing_if = "Option::is_none")]
1282    #[serde(default)]
1283    pub wasm_opt: Option<WasmOpt>,
1284
1285    /// List of build time environment variables.
1286    ///
1287    /// format in template: `{{ YOUR_VAR }}`
1288    #[serde(skip_serializing_if = "Option::is_none")]
1289    #[serde(default)]
1290    pub variables: Option<Vec<String>>,
1291}
1292
1293#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1294#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1295pub struct CompressionAlgorithms(pub Vec<CompressionAlgorithm>);
1296
1297impl CompressionAlgorithms {
1298    #[must_use]
1299    fn get_list(&self) -> ArrayVec<CompressionAlgorithm, 4> {
1300        let mut list = ArrayVec::<CompressionAlgorithm, 4>::new();
1301        let mut has_all = false;
1302
1303        for alg in &self.0 {
1304            if *alg == CompressionAlgorithm::All {
1305                has_all = true;
1306            } else if !list.contains(alg) {
1307                list.push(alg.clone());
1308            }
1309        }
1310
1311        if has_all {
1312            for alg in [
1313                CompressionAlgorithm::Brotli,
1314                CompressionAlgorithm::Zstd { level: 17 },
1315                CompressionAlgorithm::Deflate,
1316                CompressionAlgorithm::Gzip,
1317            ] {
1318                if !list.contains(&alg) {
1319                    list.push(alg);
1320                }
1321            }
1322        }
1323
1324        list
1325    }
1326}
1327
1328#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1329#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1330pub struct AssetsConfig {
1331    /// Relative path to assets directory.
1332    #[serde(skip_serializing_if = "Option::is_none")]
1333    #[serde(default)]
1334    pub dir_path: Option<String>,
1335
1336    /// Note: must start with a `/` and cannot end with a `/` unless
1337    /// `/` is the entire route.
1338    #[serde(default = "AssetsConfig::default_base_route")]
1339    pub base_route: String,
1340
1341    /// whether to add `index.html` to the base route
1342    /// and the end of routes having a trailing slash
1343    /// or missing an extension.
1344    ///
1345    /// i.e. `https://example.com/static`, `https://example.com/static/`,
1346    /// `https://example.com/static/{*path}/` and `https://example.com/static/{*path}`
1347    /// (with no extension) would return the contents of
1348    /// `/static/index.html` or `/static/{*path}/index.html`.
1349    ///
1350    /// primarily useful when serving a generated static site.
1351    ///
1352    /// Note: cannot be used with `append_index_ext`
1353    #[serde(skip_serializing_if = "Option::is_none")]
1354    #[serde(default)]
1355    pub append_index_html: Option<bool>,
1356
1357    /// whether the `{base_route}` and `{base_route}/` routes should
1358    /// skip returning the `index.html` at the root of the static dir.
1359    ///
1360    /// useful when the static dir `base_route` is set to `/` but you'd
1361    /// like to have a template use the `/` route.
1362    #[serde(skip_serializing_if = "Option::is_none")]
1363    #[serde(default)]
1364    pub skip_base_route_index_html: Option<bool>,
1365
1366    /// whether to append `.html` to routes that do not
1367    /// include an extension.
1368    ///
1369    /// i.e. `https://example.com/static/about` would return the contents of
1370    /// `/static/about.html`.
1371    ///
1372    /// primarily useful when serving a generated static site.
1373    ///
1374    /// Note: cannot be used with `append_index_html`
1375    #[serde(skip_serializing_if = "Option::is_none")]
1376    #[serde(default)]
1377    pub append_html_ext: Option<bool>,
1378
1379    /// will not strip the exif data from images.
1380    ///
1381    /// Important: only use if you want all metadata (including
1382    /// location data) on your photos.
1383    #[serde(skip_serializing_if = "Option::is_none")]
1384    #[serde(default)]
1385    pub preserve_exif: Option<bool>,
1386
1387    /// content security policy for HTML assets
1388    #[serde(skip_serializing_if = "Option::is_none")]
1389    #[serde(default)]
1390    pub html_csp: Option<HttpCsp>,
1391
1392    /// HTTP cache configuration.
1393    ///
1394    /// TODO: provide a way to pattern match files for which to apply
1395    /// TODO: specific cache controls.
1396    #[serde(skip_serializing_if = "Option::is_none")]
1397    #[serde(default)]
1398    pub http: Option<HttpCache>,
1399
1400    /// Which encodings to use when precompressing assets.
1401    ///
1402    /// Serving priority is dictated by specified order.
1403    #[serde(skip_serializing_if = "Option::is_none")]
1404    #[serde(default)]
1405    pub precompression: Option<CompressionAlgorithms>,
1406
1407    #[serde(skip_serializing)]
1408    #[serde(default)]
1409    pub internal_precompression: Option<ArrayVec<CompressionAlgorithm, 4>>,
1410
1411    /// Determines whether CSS files should be minified,
1412    /// prior to write.
1413    #[serde(skip_serializing_if = "Option::is_none")]
1414    #[serde(default)]
1415    pub minify_css: Option<bool>,
1416
1417    /// Determines whether JS files should be minified,
1418    /// prior to write.
1419    #[serde(skip_serializing_if = "Option::is_none")]
1420    #[serde(default)]
1421    pub minify_js: Option<bool>,
1422
1423    /// Determines whether HTML files should be minified,
1424    /// prior to write.
1425    #[serde(skip_serializing_if = "Option::is_none")]
1426    #[serde(default)]
1427    pub minify_html: Option<bool>,
1428
1429    #[serde(skip_serializing_if = "Option::is_none")]
1430    #[serde(default)]
1431    pub internal_cache_control_header_value: Option<String>,
1432}
1433
1434impl AssetsConfig {
1435    fn default_base_route() -> String {
1436        "/assets".to_string()
1437    }
1438}
1439
1440impl AssetsConfig {
1441    pub fn init(&mut self, default_cache_control_header_value: &str) -> anyhow::Result<()> {
1442        if let Some(http_cache) = &self.http
1443            && let Some(http_cache_control) = &http_cache.cache_control
1444        {
1445            let mut header = String::new();
1446            http_cache_control.header_value(&mut header, default_cache_control_header_value)?;
1447
1448            self.internal_cache_control_header_value = Some(header);
1449        }
1450
1451        Ok(())
1452    }
1453}
1454
1455#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1456#[derive(Deserialize, Serialize, Debug, Clone)]
1457pub struct FragmentsConfig {
1458    /// Relative path to fragments directory.
1459    pub dir_path: String,
1460}
1461
1462/// Global constant definitions, for use in [`ordinary_template::Template`]s.
1463#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1464#[derive(Deserialize, Serialize, Debug, Clone)]
1465pub struct Global {
1466    /// Name of the global constant.
1467    pub name: String,
1468    /// Type definition for the global variable.
1469    pub kind: Kind,
1470    /// JSON value of the global constant.
1471    pub value: serde_json::Value,
1472}
1473
1474/// Where Ordinary will look for the secret.
1475#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1476#[derive(Deserialize, Serialize, Debug, Clone)]
1477pub enum SecretSource {
1478    /// Name of the host provided environment variable/secret.
1479    ///
1480    /// `Env` secrets are available to every tenant for a given
1481    /// API server (when running in "multi" mode).
1482    ///
1483    /// This is useful in scenarios where a provider running the API
1484    /// server would like to expose convenient integrations with 3rd parties, for
1485    /// which it only wants to maintain a single set of credentials.
1486    ///
1487    /// `Env` secrets are also useful when you're running a standalone
1488    /// application and do not need a more complicated secrets management
1489    /// paradigm.
1490    Env,
1491    /// Name of a stored secret.
1492    ///
1493    /// `Stored` secrets are application scoped, and can be set
1494    /// through an API server on which the application runs.
1495    ///
1496    /// `Stored` secrets live in their own database and are only
1497    /// accessible to components with permissions.
1498    Stored,
1499    // `Manager` mode allows you to select an external secrets
1500    // manager, by setting a `Stored` secret with the external `Manager`'s
1501    // token/password.
1502    // todo: Manager
1503}
1504
1505/// Where the secret is accessible from.
1506#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1507#[derive(Deserialize, Serialize, Debug, Clone)]
1508pub enum SecretVisibility {
1509    // `Actions` visibility level should only be set in cases
1510    // where you're confident in the code that's being executed,
1511    // or the secret is not shared between trusted/untrusted modules.
1512    //
1513    // Because `Action` visible secrets are passed into the module,
1514    // they can be (intentionally OR unintentionally) leaked if
1515    // included in what's returned by the module.
1516    // todo: Actions,
1517    /// `Integrations` level visibility runs the risk of unintentionally
1518    /// sending a secret to the incorrect endpoint, but does not expose
1519    /// secrets to any user defined modules.
1520    Integrations,
1521}
1522
1523/// Mechanism for exposing secrets to `Integration`s.
1524#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1525#[derive(Deserialize, Serialize, Debug, Clone)]
1526pub struct Secret {
1527    /// Name of the secret.
1528    pub name: String,
1529    /// Where to retrieve the secret from.
1530    pub source: SecretSource,
1531    /// Where the secret is to be used.
1532    pub visibility: SecretVisibility,
1533}
1534
1535/// Option for the feature flag.
1536#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1537#[derive(Deserialize, Serialize, Debug, Clone)]
1538pub struct FlagOption {
1539    /// Unique index for this flag option.
1540    pub idx: u8,
1541    /// Name of this feature flag option.
1542    pub name: String,
1543    /// Percentage of users that will have this
1544    /// flag turned on.
1545    pub percentage: u8,
1546}
1547
1548/// Feature flag definition.
1549///
1550/// Note: Flags use cookies for non-logged in users,
1551/// and use fields set on their token after they're logged in
1552/// so that users have the ability to configure their preference
1553/// if they have one.
1554#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1555#[derive(Deserialize, Serialize, Debug, Clone)]
1556pub struct Flag {
1557    /// Unique index for the feature flag.
1558    pub idx: u8,
1559    /// Name of the feature flag.
1560    pub name: String,
1561    /// Options for this flag. Percentage must
1562    /// total 100.
1563    pub options: Vec<FlagOption>,
1564}
1565
1566#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1567#[derive(Deserialize, Serialize, Debug, Clone)]
1568pub struct ContentDefinition {
1569    /// Unique index for the content definition.
1570    pub idx: u8,
1571
1572    /// Name of the content definition.
1573    pub name: String,
1574
1575    /// Fields for the content definition.
1576    ///
1577    /// Note: only fields with the `String` and `Uuid` kinds can
1578    /// be indexed (for now).
1579    pub fields: Vec<Field>,
1580
1581    /// configure scripts for responding to object lifecycle events.
1582    #[serde(skip_serializing_if = "Option::is_none")]
1583    #[serde(default)]
1584    pub lifecycle: Option<ContentObjectLifecycle>,
1585}
1586
1587#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1588#[derive(Deserialize, Serialize, Debug, Clone)]
1589pub struct ContentObjectLifecycle {
1590    /// run script before all commands in the set
1591    #[serde(skip_serializing_if = "Option::is_none")]
1592    #[serde(default)]
1593    pub before_all: Option<Vec<Vec<String>>>,
1594    /// hook for running scripts before/after an object
1595    /// is added via CLI or studio.
1596    ///
1597    /// `after` receives the stringified JSON object as the
1598    /// first stdarg.
1599    #[serde(skip_serializing_if = "Option::is_none")]
1600    #[serde(default)]
1601    pub on_add: Option<LifecycleBeforeAfterScripts>,
1602    /// hook for running scripts before/after an object
1603    /// is edited via CLI or studio.
1604    ///
1605    /// `after` receives the stringified JSON object as the
1606    /// first stdarg.
1607    #[serde(skip_serializing_if = "Option::is_none")]
1608    #[serde(default)]
1609    pub on_edit: Option<LifecycleBeforeAfterScripts>,
1610    /// hook for running scripts before/after an object
1611    /// is deleted via CLI or studio.
1612    ///
1613    /// `after` receives the stringified JSON object as the
1614    /// first stdarg.
1615    #[serde(skip_serializing_if = "Option::is_none")]
1616    #[serde(default)]
1617    pub on_delete: Option<LifecycleBeforeAfterScripts>,
1618}
1619
1620/// Content is used for static values that should be updated
1621/// independent of document structure, stylings or behavior.
1622///
1623/// Content is stored in a denormalized format for indexed fields
1624/// to optimize for maximum read efficiency.
1625#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1626#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1627pub struct Content {
1628    /// Specifies the path to the JSON file which contains
1629    /// the content objects.
1630    pub file_path: String,
1631    /// Definitions/structure of the content objects in the
1632    /// `content.json`.
1633    pub definitions: Vec<ContentDefinition>,
1634
1635    #[serde(skip_serializing_if = "Option::is_none")]
1636    #[serde(default)]
1637    pub update: Option<ContentUpdateConfig>,
1638}
1639
1640#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1641#[derive(Deserialize, Serialize, Debug, Clone)]
1642pub struct ContentUpdateConfig {
1643    #[serde(skip_serializing_if = "Option::is_none")]
1644    #[serde(default)]
1645    pub lifecycle: Option<LifecycleBeforeAfterScripts>,
1646}
1647
1648#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1649#[derive(Deserialize, Serialize, Debug, Clone)]
1650pub enum UuidVersion {
1651    V4,
1652    V7,
1653}
1654
1655/// Defines a model in the Ordinary Database.
1656#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1657#[derive(Deserialize, Serialize, Debug, Clone)]
1658pub struct ModelConfig {
1659    /// Index of the model. Used for its kind
1660    /// for storage keys. There can be no gaps in
1661    /// index values.
1662    ///
1663    /// Note: the first (0) index is always skipped as it
1664    /// is reserved for the item UUID.
1665    pub idx: u8,
1666    /// Name of the model.
1667    pub name: String,
1668    /// Fields on the model.
1669    pub fields: Vec<Field>,
1670    /// Every model is generated with a UUID key. If this value
1671    /// is blank, it will default to V4. If you'd like records to
1672    /// be ordered by time, V7 is recommended.
1673    #[serde(skip_serializing_if = "Option::is_none")]
1674    #[serde(default)]
1675    pub uuid: Option<UuidVersion>,
1676}
1677
1678#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1679#[derive(Deserialize, Serialize, Debug, Clone)]
1680pub enum IntegrationProtocolHttpEncoding {
1681    Json,
1682    Text,
1683    None,
1684}
1685
1686/// The protocol for the integration.
1687#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1688#[derive(Deserialize, Serialize, Debug, Clone)]
1689pub enum IntegrationProtocol {
1690    /// For integrating an external HTTP API.
1691    Http {
1692        /// HTTP method.
1693        method: String,
1694        /// static http headers
1695        headers: Vec<(String, String)>,
1696        /// how to encode the value passed from the action
1697        send_encoding: IntegrationProtocolHttpEncoding,
1698        /// how to decode the value passed back to the action
1699        recv_encoding: IntegrationProtocolHttpEncoding,
1700    },
1701    // When integrating an external gRPC API.
1702    // todo: Grpc { metadata: Vec<(String, String)> },
1703    // When integrating an external Cap'n Proto API.
1704    // todo: CapnProto,
1705    // When integrating an external GraphQL API.
1706    // todo: GraphQL,
1707    // When integrating an external PostgreSQL database.
1708    // todo: Postgres { statement: String },
1709    // When integrating an external OpenSearch database.
1710    // todo: OpenSearch,
1711    // For integrating with SMTP mail server
1712    // todo: Smtp,
1713}
1714
1715/// The mechanism for reverse proxying and calling
1716/// out to external APIs.
1717#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1718#[derive(Deserialize, Serialize, Debug, Clone)]
1719pub struct IntegrationConfig {
1720    /// Unique index for integration
1721    pub idx: u8,
1722    /// Name of the integration.
1723    pub name: String,
1724    /// Protocol used for communicating with the external service.
1725    pub protocol: IntegrationProtocol,
1726    /// Endpoint that the external service lives at.
1727    pub endpoint: String,
1728    /// Definition for the parameters of the receiving service.
1729    ///
1730    /// (Automatically translated to the service's protocol/encoding format).
1731    pub send: Kind,
1732    /// Definition for the returned value of the external service.
1733    ///
1734    /// (Automatically translated from the service's protocol/encoding format).
1735    pub recv: Kind,
1736    /// Names of secrets to include
1737    #[serde(skip_serializing_if = "Option::is_none")]
1738    #[serde(default)]
1739    pub secrets: Option<Vec<String>>,
1740
1741    /// Max duration for the template.
1742    ///
1743    /// Unit: seconds
1744    #[serde(skip_serializing_if = "Option::is_none")]
1745    #[serde(default)]
1746    pub timeout: Option<u16>,
1747}
1748
1749/// The language in which the action is written.
1750///
1751/// Many more languages will be supported in the future.
1752#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1753#[derive(Deserialize, Serialize, Debug, Clone)]
1754pub enum ActionLang {
1755    /// Action is written in the Rust programming language.
1756    ///
1757    /// Uses zero-copy `FlexBuffer` vectors for serialization format.
1758    Rust,
1759    /// Action is written in JavaScript.
1760    ///
1761    /// `QuickJS` runtime embedded in a Rust WASM which itself uses
1762    /// `FlexBuffer` vectors for FFI serialization, but then translates
1763    /// to JSON when communicating across the `QuickJS` runtime barrier.
1764    JavaScript,
1765    // todo: Golang,
1766    // todo: DotNet,
1767    // todo: Kotlin,
1768}
1769
1770/// Model operations that an action is allowed to make.
1771#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1772#[derive(Deserialize, Serialize, Debug, Clone)]
1773pub enum ActionAccessModelOps {
1774    /// Can create an item for this model.
1775    Insert,
1776    /// Can get an item for this model by UUID or Index.
1777    Get,
1778    /// Can query an item for this model by queryable field.
1779    Query,
1780    /// Can search an item for this model by searchable field.
1781    Search,
1782    /// Can update an item for this model.
1783    Update,
1784    /// Can delete an item for this model.
1785    Delete,
1786}
1787
1788/// Auth operations that an action can make.
1789#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1790#[derive(Deserialize, Serialize, Debug, Clone)]
1791pub enum ActionAccessAuthOps {
1792    /// Allows the action the ability to set
1793    /// a user's access token fields/claims.
1794    SetTokenFields,
1795}
1796
1797/// Defines access permissions that an Ordinary
1798/// Action can configure.
1799#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1800#[derive(Deserialize, Serialize, Debug, Clone)]
1801pub enum ActionAccessPermission {
1802    /// Provides the action access to a given model.
1803    Model {
1804        /// Name of the model.
1805        name: String,
1806        /// List of allowed operations that the action
1807        /// can take on the model.
1808        ops: Vec<ActionAccessModelOps>,
1809    },
1810    /// Provides the action access to a given content def.
1811    Content {
1812        /// Content definition name.
1813        name: String,
1814    },
1815    /// Provides the action access to a given integration.
1816    Integration {
1817        /// Name of the integration the action can access.
1818        name: String,
1819    },
1820    /// Provides the action access to another action.
1821    Action {
1822        /// Name of the other action.
1823        name: String,
1824    },
1825    /// Provides the action access to Ordinary Auth.
1826    Auth {
1827        /// Which operations the action is allowed to take.
1828        ops: Vec<ActionAccessAuthOps>,
1829    },
1830}
1831
1832#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1833#[derive(Deserialize, Serialize, Debug, Clone)]
1834pub enum HttpMethod {
1835    PUT,
1836    POST,
1837    GET,
1838    DELETE,
1839}
1840
1841#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1842#[derive(Deserialize, Serialize, Debug, Clone)]
1843pub enum ActionTriggerModelOps {
1844    Insert,
1845    Update,
1846    Delete,
1847}
1848
1849/// Action trigger options.
1850#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1851#[derive(Deserialize, Serialize, Debug, Clone)]
1852pub enum ActionTrigger {
1853    /// HTTP request with the Ordinary data format
1854    Ordinary,
1855    /// JSON formatted API call
1856    Json {
1857        /// API rout
1858        route: String,
1859        /// HTTP method
1860        method: HttpMethod,
1861    },
1862    /// Web Form Submission
1863    Form {
1864        /// endpoint the form should point at
1865        route: String,
1866        /// method for the form to use
1867        method: HttpMethod,
1868        /// redirect for after submission.
1869        ///
1870        /// note: it is allowed to use return values in the route
1871        /// via {return} or {`return.some_field_name`}
1872        redirect: String,
1873    },
1874    /// Login event from Auth
1875    Login,
1876    /// Registration event from Auth
1877    Registration,
1878    /// For when content updates
1879    Content { name: String },
1880    // Run on model insert/update/delete
1881    // with affected item as argument.
1882    // todo: Model {
1883    //     name: String,
1884    //     op: ActionAccessModelOps,
1885    // },
1886
1887    // Have the action triggered on a regular cadence.
1888    // todo: Job { expression: String },
1889
1890    // gRPC formatted request
1891    // todo: Grpc,
1892}
1893
1894#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1895#[derive(Deserialize, Serialize, Debug, Clone)]
1896pub enum ActionFfiVersion {
1897    V1,
1898}
1899
1900/// Input/output serialization for module and host functions.
1901#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1902#[derive(Deserialize, Serialize, Debug, Clone)]
1903pub enum ActionFfiSerialization {
1904    FlexBufferVector,
1905}
1906
1907#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1908#[derive(Deserialize, Serialize, Debug, Clone)]
1909pub struct ActionFfi {
1910    pub version: ActionFfiVersion,
1911    pub serialization: ActionFfiSerialization,
1912}
1913
1914/// Configuration parameters for Ordinary Actions.
1915#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1916#[derive(Deserialize, Serialize, Debug, Clone)]
1917pub struct ActionConfig {
1918    /// Foreign function interface config
1919    pub ffi: ActionFfi,
1920    /// Unique index value for action.
1921    pub idx: u8,
1922    /// Action name. Must be unique.
1923    pub name: String,
1924    /// The source language the action is written in.
1925    pub lang: ActionLang,
1926    /// Relative path to the source directory for the action.
1927    #[serde(skip_serializing_if = "Option::is_none")]
1928    #[serde(default)]
1929    pub dir_path: Option<String>,
1930    /// What to check the token fields against. If blank
1931    /// action is public.
1932    #[serde(skip_serializing_if = "Option::is_none")]
1933    #[serde(default)]
1934    pub protected: Option<Check>,
1935    /// whether the storage interactions
1936    /// are executed under a single transaction.
1937    #[serde(skip_serializing_if = "Option::is_none")]
1938    #[serde(default)]
1939    pub transactional: Option<bool>,
1940    /// Which Ordinary Application resources the action has access to.
1941    pub access: Vec<ActionAccessPermission>,
1942    /// Input definition for the action.
1943    pub accepts: Kind,
1944    /// Output definition for the action.
1945    pub returns: Kind,
1946    /// How this action is called (i.e. side effect from DB/Auth,
1947    /// http API call, browser form submission, etc.)
1948    pub triggered_by: Vec<ActionTrigger>,
1949
1950    /// Max duration for the action.
1951    ///
1952    /// Unit: seconds
1953    #[serde(skip_serializing_if = "Option::is_none")]
1954    #[serde(default)]
1955    pub timeout: Option<u16>,
1956
1957    #[serde(skip_serializing_if = "Option::is_none")]
1958    #[serde(default)]
1959    pub cors: Option<HttpCors>,
1960
1961    #[serde(skip_serializing_if = "Option::is_none")]
1962    #[serde(default)]
1963    pub wasm_opt: Option<WasmOpt>,
1964
1965    /// Whether the action should have bindings for API server interaction.
1966    ///
1967    /// Can only be set on applications which have been explicitly
1968    /// allow-listed by the API server administrator via their domain.
1969    #[serde(skip_serializing_if = "Option::is_none")]
1970    #[serde(default)]
1971    pub privileged: Option<bool>,
1972
1973    /// List of build time environment variables.
1974    ///
1975    /// format in template: `{{ YOUR_VAR }}`
1976    #[serde(skip_serializing_if = "Option::is_none")]
1977    #[serde(default)]
1978    pub variables: Option<Vec<String>>,
1979}
1980
1981#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1982#[derive(Deserialize, Serialize, Debug, Clone, Default)]
1983pub struct ErrorConfig {
1984    /// Refers to the error template by name.
1985    ///
1986    /// Note: if set, will override the `asset` field
1987    #[serde(skip_serializing_if = "Option::is_none")]
1988    #[serde(default)]
1989    pub template: Option<String>,
1990
1991    /// Refers to the asset by path.
1992    ///
1993    /// Note: if `template` is set it will override this field
1994    #[serde(skip_serializing_if = "Option::is_none")]
1995    #[serde(default)]
1996    pub asset: Option<String>,
1997}
1998
1999#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2000#[derive(Deserialize, Serialize, Debug, Clone)]
2001pub enum RuntimeMode {
2002    /// Application will run on the shared multithreaded
2003    /// tokio runtime.
2004    Shared,
2005    /// Application will run on a separate thread with its
2006    /// own single-threaded tokio runtime.
2007    SingleThreaded,
2008    /// Application will run on a separate thread with its
2009    /// own multithreaded tokio runtime.
2010    MultiThreaded,
2011}
2012
2013#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2014#[derive(Deserialize, Serialize, Debug, Clone)]
2015pub enum HttpCorsAllowHeaders {
2016    Any,
2017    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers>
2018    Headers(Vec<String>),
2019}
2020
2021#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2022#[derive(Deserialize, Serialize, Debug, Clone)]
2023pub enum HttpCorsExposeHeaders {
2024    Any,
2025    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers>
2026    Headers(Vec<String>),
2027}
2028
2029#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2030#[derive(Deserialize, Serialize, Debug, Clone)]
2031pub enum HttpCorsAllowMethods {
2032    Any,
2033    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods>
2034    Methods(Vec<String>),
2035}
2036
2037#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2038#[derive(Deserialize, Serialize, Debug, Clone)]
2039pub enum HttpCorsAllowOrigin {
2040    Any,
2041    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin>
2042    Origins(Vec<String>),
2043}
2044
2045#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2046#[derive(Deserialize, Serialize, Debug, Clone, Default)]
2047pub struct HttpCors {
2048    pub allow_credentials: Option<bool>,
2049
2050    pub allow_headers: Option<HttpCorsAllowHeaders>,
2051
2052    /// unit: Seconds
2053    pub max_age: Option<u32>,
2054
2055    pub allow_methods: Option<HttpCorsAllowMethods>,
2056
2057    pub allow_origin: Option<HttpCorsAllowOrigin>,
2058
2059    pub expose_headers: Option<HttpCorsExposeHeaders>,
2060
2061    pub allow_private_network: Option<bool>,
2062}
2063
2064impl HttpCors {
2065    #[must_use]
2066    pub fn overwrite(&self, base: &Self) -> Self {
2067        Self {
2068            allow_credentials: if self.allow_credentials.is_none() {
2069                base.allow_credentials
2070            } else {
2071                self.allow_credentials
2072            },
2073            allow_headers: if self.allow_headers.is_none() {
2074                base.allow_headers.clone()
2075            } else {
2076                self.allow_headers.clone()
2077            },
2078            max_age: if self.max_age.is_none() {
2079                base.max_age
2080            } else {
2081                self.max_age
2082            },
2083            allow_methods: if self.allow_methods.is_none() {
2084                base.allow_methods.clone()
2085            } else {
2086                self.allow_methods.clone()
2087            },
2088            allow_origin: if self.allow_origin.is_none() {
2089                base.allow_origin.clone()
2090            } else {
2091                self.allow_origin.clone()
2092            },
2093            expose_headers: if self.expose_headers.is_none() {
2094                base.expose_headers.clone()
2095            } else {
2096                self.expose_headers.clone()
2097            },
2098            allow_private_network: if self.allow_private_network.is_none() {
2099                base.allow_private_network
2100            } else {
2101                self.allow_private_network
2102            },
2103        }
2104    }
2105}
2106
2107#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2108#[derive(Deserialize, Serialize, Debug, Clone, Default)]
2109pub struct HttpCsp {
2110    /// defaults to `'self'` (`default-src ` does not need to be included)
2111    #[serde(skip_serializing_if = "Option::is_none")]
2112    #[serde(default)]
2113    pub default_src: Option<String>,
2114
2115    /// defaults to unset unless inline hashes are included,
2116    /// in which case the directive will start with `'self'` (`script-src ` does not need to be included).
2117    #[serde(skip_serializing_if = "Option::is_none")]
2118    #[serde(default)]
2119    pub script_src: Option<String>,
2120
2121    /// defaults to unset unless inline hashes are included,
2122    /// in which case the directive will start with `'self'` (`style-src ` does not need to be included).
2123    #[serde(skip_serializing_if = "Option::is_none")]
2124    #[serde(default)]
2125    pub style_src: Option<String>,
2126
2127    /// defaults to unset.
2128    ///
2129    /// (`font-src ` does not need to be included).
2130    #[serde(skip_serializing_if = "Option::is_none")]
2131    #[serde(default)]
2132    pub font_src: Option<String>,
2133
2134    /// defaults to unset.
2135    ///
2136    /// (`img-src ` does not need to be included).
2137    #[serde(skip_serializing_if = "Option::is_none")]
2138    #[serde(default)]
2139    pub img_src: Option<String>,
2140
2141    /// defaults to unset.
2142    ///
2143    /// (`frame-src ` does not need to be included).
2144    #[serde(skip_serializing_if = "Option::is_none")]
2145    #[serde(default)]
2146    pub frame_src: Option<String>,
2147
2148    /// defaults to `true`.
2149    #[serde(skip_serializing_if = "Option::is_none")]
2150    #[serde(default)]
2151    pub include_inline_hashes: Option<bool>,
2152}
2153
2154impl HttpCsp {
2155    #[must_use]
2156    #[allow(clippy::too_many_lines)]
2157    pub fn build_string(
2158        &self,
2159        base: &Self,
2160        inline_style_hashes: Option<Vec<String>>,
2161        inline_script_hashes: Option<Vec<String>>,
2162        script_urls: Option<Vec<String>>,
2163        secure: bool,
2164        has_wasm: bool,
2165    ) -> String {
2166        let mut out = String::new();
2167
2168        let include_inline_hashes = self
2169            .include_inline_hashes
2170            .unwrap_or(base.include_inline_hashes.unwrap_or(true));
2171
2172        let default_src = self
2173            .default_src
2174            .clone()
2175            .unwrap_or(base.default_src.clone().unwrap_or("'self'".to_string()));
2176
2177        if !default_src.is_empty() {
2178            out.push_str("default-src ");
2179            out.push_str(default_src.as_str());
2180            out.push_str("; ");
2181        }
2182
2183        let mut script_src = self
2184            .script_src
2185            .clone()
2186            .unwrap_or(base.script_src.clone().unwrap_or_default());
2187
2188        if include_inline_hashes
2189            && let Some(script_hashes) = inline_script_hashes
2190            && !script_hashes.is_empty()
2191        {
2192            if script_src.is_empty() {
2193                script_src.push_str("'self'");
2194                if has_wasm {
2195                    script_src.push_str(" 'wasm-unsafe-eval'");
2196                }
2197            }
2198
2199            for hash in script_hashes {
2200                script_src.push_str(" '");
2201                script_src.push_str(hash.as_str());
2202                script_src.push('\'');
2203            }
2204        }
2205
2206        if let Some(script_urls) = script_urls {
2207            if script_src.is_empty() {
2208                script_src.push_str("'self'");
2209            }
2210
2211            for script_url in script_urls {
2212                script_src.push(' ');
2213                script_src.push_str(&script_url);
2214            }
2215        }
2216
2217        if !script_src.is_empty() {
2218            out.push_str("script-src ");
2219            out.push_str(script_src.as_str());
2220            out.push_str("; ");
2221        }
2222
2223        let mut style_src = self
2224            .style_src
2225            .clone()
2226            .unwrap_or(base.style_src.clone().unwrap_or_default());
2227
2228        if include_inline_hashes
2229            && let Some(style_hashes) = inline_style_hashes
2230            && !style_hashes.is_empty()
2231        {
2232            if style_src.is_empty() {
2233                style_src.push_str("'self'");
2234            }
2235
2236            for hash in style_hashes {
2237                style_src.push_str(" '");
2238                style_src.push_str(hash.as_str());
2239                style_src.push('\'');
2240            }
2241        }
2242
2243        if !style_src.is_empty() {
2244            out.push_str("style-src ");
2245            out.push_str(style_src.as_str());
2246            out.push_str("; ");
2247        }
2248
2249        let font_src = self
2250            .font_src
2251            .clone()
2252            .unwrap_or(base.font_src.clone().unwrap_or_default());
2253        if !font_src.is_empty() {
2254            out.push_str("font-src ");
2255            out.push_str(font_src.as_str());
2256            out.push_str("; ");
2257        }
2258
2259        let img_src = self
2260            .img_src
2261            .clone()
2262            .unwrap_or(base.img_src.clone().unwrap_or_default());
2263        if !img_src.is_empty() {
2264            out.push_str("img-src ");
2265            out.push_str(img_src.as_str());
2266            out.push_str("; ");
2267        }
2268
2269        let frame_src = self
2270            .frame_src
2271            .clone()
2272            .unwrap_or(base.frame_src.clone().unwrap_or_default());
2273        if !frame_src.is_empty() {
2274            out.push_str("frame-src ");
2275            out.push_str(frame_src.as_str());
2276            out.push_str("; ");
2277        }
2278
2279        if secure {
2280            out.push_str("upgrade-insecure-requests; ");
2281        }
2282
2283        out.push_str("report-to csp");
2284
2285        out = out.trim().to_string();
2286        out
2287    }
2288}
2289
2290#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2291#[derive(Deserialize, Serialize, Debug, Clone)]
2292pub struct LifecycleBeforeAfterScripts {
2293    #[serde(skip_serializing_if = "Option::is_none")]
2294    #[serde(default)]
2295    pub before: Option<Vec<Vec<String>>>,
2296    #[serde(skip_serializing_if = "Option::is_none")]
2297    #[serde(default)]
2298    pub after: Option<Vec<Vec<String>>>,
2299}
2300
2301#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2302#[derive(Deserialize, Serialize, Debug, Clone)]
2303pub struct TopLevelLifecycle {
2304    /// run before every lifecycle operation
2305    pub before_all: Option<Vec<Vec<String>>>,
2306
2307    /// configure build lifecycle hooks
2308    #[serde(skip_serializing_if = "Option::is_none")]
2309    #[serde(default)]
2310    pub build: Option<LifecycleBeforeAfterScripts>,
2311}
2312
2313#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2314#[derive(Deserialize, Serialize, Debug, Clone)]
2315pub enum RedirectMethod {
2316    /// 307 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/307>
2317    Temporary,
2318    /// 308 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/308>
2319    Permanent,
2320}
2321
2322impl Display for RedirectMethod {
2323    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2324        match self {
2325            Self::Temporary => write!(f, "TEMPORARY"),
2326            Self::Permanent => write!(f, "PERMANENT"),
2327        }
2328    }
2329}
2330
2331#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2332#[derive(Deserialize, Serialize, Debug, Clone)]
2333pub struct HostRedirect {
2334    /// from host name
2335    pub from: String,
2336    /// to host name
2337    pub to: String,
2338    /// redirect method
2339    pub method: RedirectMethod,
2340}
2341
2342#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2343#[derive(Deserialize, Serialize, Debug, Clone)]
2344pub struct RouteRedirect {
2345    /// [axum route](https://docs.rs/axum/latest/axum/struct.Router.html#method.route) pattern
2346    pub condition: String,
2347    /// translation regexes map to [`Regex::replace_all`](https://docs.rs/regex/latest/regex/#example-replacement-with-named-capture-groups)
2348    /// where `rule.0` is body of `Regex::new()` and `rule.1` is second param of `re.replace_all()`.
2349    pub rule: (String, String),
2350    /// redirect method
2351    pub method: RedirectMethod,
2352}
2353
2354#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2355#[derive(Deserialize, Serialize, Debug, Clone)]
2356pub struct Redirects {
2357    /// host redirects
2358    #[serde(skip_serializing_if = "Option::is_none")]
2359    #[serde(default)]
2360    pub host: Option<Vec<HostRedirect>>,
2361    /// route redirects
2362    #[serde(skip_serializing_if = "Option::is_none")]
2363    #[serde(default)]
2364    pub route: Option<Vec<RouteRedirect>>,
2365}
2366
2367/// Config definition for an Ordinary Application
2368#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2369#[derive(Deserialize, Serialize, Debug, Clone, Default)]
2370pub struct OrdinaryConfig {
2371    #[serde(skip_serializing_if = "Option::is_none")]
2372    #[serde(default)]
2373    pub lifecycle: Option<TopLevelLifecycle>,
2374
2375    /// Domain name for the application to be run from the
2376    /// deployment environment.
2377    pub domain: String,
2378
2379    /// additional domains with a CNAME or ALIAS records
2380    /// pointing at the primary `OrdinaryConfig::domain`.
2381    ///
2382    /// add a TXT record in the following format:
2383    ///`ordinary=your.config.domain`
2384    #[serde(skip_serializing_if = "Option::is_none")]
2385    #[serde(default)]
2386    pub cnames: Option<Vec<String>>,
2387
2388    /// specify which of the `domain` or `cnames` is
2389    /// the "canonical" location.
2390    ///
2391    /// this is useful for [indexing](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
2392    /// and situations where you want to display the primary
2393    /// URL as text on the page itself (i.e. pick one of `example.some.host`, `example.com`, and `www.example.com`).
2394    ///
2395    /// defaults to `domain` if `cnames` is empty. defaults to first `cname` in list if `cnames`
2396    /// are not empty.
2397    #[serde(skip_serializing_if = "Option::is_none")]
2398    #[serde(default)]
2399    pub canonical: Option<String>,
2400
2401    #[serde(skip_serializing_if = "Option::is_none")]
2402    #[serde(default)]
2403    pub redirects: Option<Redirects>,
2404
2405    /// list of email addresses that can be used to contact
2406    /// the application owner or administrators.
2407    #[serde(skip_serializing_if = "Option::is_none")]
2408    #[serde(default)]
2409    pub contacts: Option<Vec<String>>,
2410
2411    /// whether contacts should be hidden (defaults to `true`)
2412    #[serde(skip_serializing_if = "Option::is_none")]
2413    #[serde(default)]
2414    pub hide_contacts: Option<bool>,
2415
2416    /// Version of the site build.
2417    pub version: String,
2418
2419    /// Storage size in bytes (rounded up to nearest OS page size).
2420    #[serde(skip_serializing_if = "Option::is_none")]
2421    #[serde(default = "OrdinaryConfig::default_storage_size")]
2422    pub storage_size: Option<u64>,
2423
2424    /// Default request timeout.
2425    ///
2426    /// Unit (seconds).
2427    #[serde(skip_serializing_if = "Option::is_none")]
2428    #[serde(default)]
2429    pub default_timeout: Option<u16>,
2430
2431    /// HTTP Content Security Policy configuration.
2432    ///
2433    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>
2434    ///
2435    /// "Base" defaults to `default-src 'self';` and tacks on SHA-256 integrity
2436    /// hashes for all inlined scripts and styles (generated at build time) to
2437    /// `script-src 'self' sha256-b64` and `style-src 'self' sha256-b64`, respectively.
2438    ///
2439    /// `https:` is used when not running in `--insecure` mode.
2440    #[serde(skip_serializing_if = "Option::is_none")]
2441    #[serde(default)]
2442    pub csp: Option<HttpCsp>,
2443
2444    #[serde(skip_serializing_if = "Option::is_none")]
2445    #[serde(default)]
2446    pub cors: Option<HttpCors>,
2447
2448    /// Specifies runtime mode for application on the host.
2449    ///
2450    /// If none is specified, defaults to Shared (or host default).
2451    #[serde(skip_serializing_if = "Option::is_none")]
2452    #[serde(default)]
2453    pub runtime: Option<RuntimeMode>,
2454
2455    /// When set to true, `{{ domain }}/.ordinary/schema`
2456    /// is not addressable.
2457    ///
2458    /// Note: this can break applications which depend on
2459    /// flags, and `action`/`template` query descriptors.
2460    #[serde(skip_serializing_if = "Option::is_none")]
2461    #[serde(default)]
2462    pub hide_schema: Option<bool>,
2463
2464    /// Include template rendering code in the client WASM.
2465    #[serde(skip_serializing_if = "Option::is_none")]
2466    #[serde(default)]
2467    pub client_rendering: Option<bool>,
2468
2469    /// Include E2EE handler code in the client WASM.
2470    #[serde(skip_serializing_if = "Option::is_none")]
2471    #[serde(default)]
2472    pub obfuscation: Option<bool>,
2473
2474    /// Include E2EE handler code in the client WASM.
2475    #[serde(skip_serializing_if = "Option::is_none")]
2476    #[serde(default)]
2477    pub client_events: Option<bool>,
2478
2479    /// Port to be used for standalone "run" instances.
2480    #[serde(skip_serializing_if = "Option::is_none")]
2481    #[serde(default)]
2482    pub port: Option<u16>,
2483    /// port used for redirecting from http when
2484    /// standalone is running in secure mode.
2485    #[serde(skip_serializing_if = "Option::is_none")]
2486    #[serde(default)]
2487    pub redirect_port: Option<u16>,
2488
2489    #[serde(skip_serializing_if = "Option::is_none")]
2490    #[serde(default)]
2491    pub logging: Option<LoggingConfig>,
2492    /// Configures error handling.
2493    ///
2494    /// Note: If not included just the error message will be
2495    /// sent back as text.
2496    #[serde(skip_serializing_if = "Option::is_none")]
2497    #[serde(default)]
2498    pub error: Option<ErrorConfig>,
2499    /// Auth config for the Ordinary application.
2500    #[serde(skip_serializing_if = "Option::is_none")]
2501    #[serde(default)]
2502    pub auth: Option<AuthConfig>,
2503    /// Global constants that can be accessed from templates
2504    #[serde(skip_serializing_if = "Option::is_none")]
2505    #[serde(default)]
2506    pub globals: Option<Vec<Global>>,
2507    /// Secrets that can be used by actions or integrations.
2508    #[serde(skip_serializing_if = "Option::is_none")]
2509    #[serde(default)]
2510    pub secrets: Option<Vec<Secret>>,
2511    /// Feature flags which can be referenced from templates
2512    /// to inform application behavior, and run experiments.
2513    #[serde(skip_serializing_if = "Option::is_none")]
2514    #[serde(default)]
2515    pub flags: Option<Vec<Flag>>,
2516    /// Definitions for static content "types"/object structure
2517    /// that can be used to inform template/page development (i.e.
2518    /// one might define a "post" content definition, and then create
2519    /// a template for their blog).
2520    #[serde(skip_serializing_if = "Option::is_none")]
2521    #[serde(default)]
2522    pub content: Option<Content>,
2523    /// Definitions for the models that will be stored in the Ordinary database.
2524    #[serde(skip_serializing_if = "Option::is_none")]
2525    #[serde(default)]
2526    pub models: Option<Vec<ModelConfig>>,
2527    /// Definitions for the external APIs that will be integrated
2528    /// into the Ordinary application.
2529    #[serde(skip_serializing_if = "Option::is_none")]
2530    #[serde(default)]
2531    pub integrations: Option<Vec<IntegrationConfig>>,
2532    /// IO, access and language configuration for actions that
2533    /// are compiled to and executed as WebAssembly modules.
2534    #[serde(skip_serializing_if = "Option::is_none")]
2535    #[serde(default)]
2536    pub actions: Option<Vec<ActionConfig>>,
2537    /// Specifies the asset directory and per-path configuration
2538    /// details for assets that require preprocessing (TypeScript, SCSS,
2539    /// JavaScript minification, etc.)
2540    #[serde(skip_serializing_if = "Option::is_none")]
2541    #[serde(default)]
2542    pub assets: Option<AssetsConfig>,
2543    /// Configuration for the template fragments
2544    #[serde(skip_serializing_if = "Option::is_none")]
2545    #[serde(default)]
2546    pub fragments: Option<FragmentsConfig>,
2547    /// Configuration for the templates/pages that the application
2548    /// will render. Each template is compiled to a WebAssembly module
2549    /// which accepts runtime arguments for models/content/integrations, and can be
2550    /// executed on either the server or the client.
2551    ///
2552    /// With the option to render on the client, only the result of the server
2553    /// query needs to be sent, in a compact, optimized, format.
2554    ///
2555    /// Currently, all rendering is happening server-side, and only HTML is being sent.
2556    ///
2557    /// In an ideal/future state multiple modes will be supported, even up to a full
2558    /// 'noscript' config.
2559    #[serde(skip_serializing_if = "Option::is_none")]
2560    #[serde(default)]
2561    pub templates: Option<Vec<TemplateConfig>>,
2562}
2563
2564impl OrdinaryConfig {
2565    /// gets ordinary.json from project path and deserializes to struct.
2566    pub fn get(proj_path: &str) -> anyhow::Result<OrdinaryConfig> {
2567        let path = Path::new(proj_path).join("ordinary.json");
2568        let config_json = fs_err::read_to_string(&path)?;
2569
2570        let mut config = match serde_json::from_str::<OrdinaryConfig>(config_json.as_str()) {
2571            Ok(config) => config,
2572            Err(err) => bail!("{}: {err}", path.display()),
2573        };
2574
2575        config.load_internal_compression();
2576
2577        Ok(config)
2578    }
2579
2580    pub fn load_internal_compression(&mut self) {
2581        if let Some(assets) = self.assets.as_mut()
2582            && let Some(precompression) = &assets.precompression
2583        {
2584            assets.internal_precompression = Some(precompression.get_list());
2585        }
2586
2587        if let Some(templates) = self.templates.as_mut() {
2588            for template in templates {
2589                if let Some(cache) = template.cache.as_mut()
2590                    && let Some(stored) = cache.stored.as_mut()
2591                    && let Some(compression) = &stored.compression
2592                {
2593                    stored.internal_compression = Some(compression.get_list());
2594                }
2595            }
2596        }
2597    }
2598
2599    /// gets ordinary.json from project path, deserializes to struct and
2600    /// strips out all client-only values.
2601    pub fn for_send(&self) -> anyhow::Result<OrdinaryConfig> {
2602        let mut config = self.clone();
2603
2604        config.lifecycle = None;
2605
2606        if let Some(assets) = config.assets.as_mut() {
2607            assets.dir_path = None;
2608        }
2609
2610        if let Some(content) = config.content.as_mut() {
2611            content.update = None;
2612
2613            for def in &mut content.definitions {
2614                def.lifecycle = None;
2615            }
2616        }
2617
2618        if let Some(templates) = config.templates.as_mut() {
2619            for template in templates {
2620                template.path = None;
2621                template.wasm_opt = None;
2622                template.minify = None;
2623            }
2624        }
2625
2626        if let Some(actions) = config.actions.as_mut() {
2627            for action in actions {
2628                action.wasm_opt = None;
2629                action.dir_path = None;
2630            }
2631        }
2632
2633        Ok(config)
2634    }
2635
2636    /// check that all configuration values are internally consistent
2637    /// and no non-existent properties or fields are used.
2638    #[instrument(skip_all, err, level = "debug")]
2639    pub fn validate(&self) -> anyhow::Result<()> {
2640        validate(self)
2641    }
2642
2643    // defaults
2644    #[must_use]
2645    #[allow(clippy::unnecessary_wraps)]
2646    pub fn default_storage_size() -> Option<u64> {
2647        Some(5_000_000)
2648    }
2649    // end defaults
2650
2651    #[must_use]
2652    pub fn has_ordinary_actions(&self) -> bool {
2653        if let Some(actions) = &self.actions {
2654            actions
2655                .iter()
2656                .find(|a| {
2657                    for trigger in &a.triggered_by {
2658                        if let ActionTrigger::Ordinary = trigger {
2659                            return true;
2660                        }
2661                    }
2662
2663                    false
2664                })
2665                .is_some()
2666        } else {
2667            false
2668        }
2669    }
2670
2671    /// Check that all the configuration properties are within API specified
2672    /// limits.
2673    ///
2674    /// Note: privileged domains are not subject to limitations checks.
2675    #[allow(clippy::too_many_lines)]
2676    pub fn check_config_against_limits(
2677        &self,
2678        limits: &OrdinaryApiLimits,
2679        privileged_domains: &HashSet<String>,
2680    ) -> anyhow::Result<()> {
2681        if privileged_domains.contains(&self.domain) {
2682            return Ok(());
2683        }
2684
2685        if let Some(canonical) = &self.canonical {
2686            let mut is_valid = false;
2687
2688            if canonical == &self.domain {
2689                is_valid = true;
2690            }
2691
2692            if let Some(cnames) = &self.cnames {
2693                for cname in cnames {
2694                    if cname == canonical {
2695                        is_valid = true;
2696                        break;
2697                    }
2698                }
2699            }
2700
2701            if !is_valid {
2702                bail!("canonical: {canonical}, is not in 'domain' or 'cnames'");
2703            }
2704        }
2705
2706        if let Some(default_timeout) = self.default_timeout
2707            && default_timeout > limits.max_default_timeout
2708        {
2709            bail!(
2710                "default timeout {} greater than limit {}",
2711                default_timeout,
2712                limits.max_default_timeout
2713            );
2714        }
2715
2716        if let Some(templates) = &self.templates {
2717            if templates.len() > limits.template.count as usize {
2718                bail!(
2719                    "template count {} greater than limit {}",
2720                    templates.len(),
2721                    limits.template.count
2722                );
2723            }
2724
2725            for template in templates {
2726                if let Some(timeout) = template.timeout
2727                    && timeout > limits.template.max_timeout
2728                {
2729                    bail!(
2730                        "template timeout {} greater than limit {} for {}",
2731                        timeout,
2732                        limits.template.max_timeout,
2733                        template.name
2734                    );
2735                }
2736
2737                if let Some(cache) = &template.cache
2738                    && let Some(stored_cache) = &cache.stored
2739                {
2740                    if let Some(max_size) = stored_cache.max_size
2741                        && (max_size < limits.storage.cache.max_size_range.0
2742                            || max_size > limits.storage.cache.max_size_range.1)
2743                    {
2744                        bail!(
2745                            "template cache max_size {} is not within range [{}, {}] for {}",
2746                            max_size,
2747                            limits.storage.cache.max_size_range.0,
2748                            limits.storage.cache.max_size_range.1,
2749                            template.name
2750                        );
2751                    }
2752
2753                    if let Some(max_count) = stored_cache.max_count
2754                        && (max_count < limits.storage.cache.max_count_range.0
2755                            || max_count > limits.storage.cache.max_count_range.1)
2756                    {
2757                        bail!(
2758                            "template cache max_count {} is not within range [{}, {}] for {}",
2759                            max_count,
2760                            limits.storage.cache.max_count_range.0,
2761                            limits.storage.cache.max_count_range.1,
2762                            template.name
2763                        );
2764                    }
2765
2766                    if let Some((clean_interval_min, clean_interval_max)) =
2767                        stored_cache.clean_interval
2768                    {
2769                        if clean_interval_min < limits.storage.cache.clean_interval_ranges.0.0
2770                            || clean_interval_min > limits.storage.cache.clean_interval_ranges.0.1
2771                        {
2772                            bail!(
2773                                "template cache clean_interval min {} is not within range [{}, {}] for {}",
2774                                clean_interval_min,
2775                                limits.storage.cache.clean_interval_ranges.0.0,
2776                                limits.storage.cache.clean_interval_ranges.0.1,
2777                                template.name
2778                            );
2779                        }
2780
2781                        if clean_interval_max < limits.storage.cache.clean_interval_ranges.1.0
2782                            || clean_interval_max > limits.storage.cache.clean_interval_ranges.1.1
2783                        {
2784                            bail!(
2785                                "template cache clean_interval min {} is not within range [{}, {}] for {}",
2786                                clean_interval_max,
2787                                limits.storage.cache.clean_interval_ranges.1.0,
2788                                limits.storage.cache.clean_interval_ranges.1.1,
2789                                template.name
2790                            );
2791                        }
2792                    }
2793                }
2794            }
2795        }
2796
2797        if let Some(integrations) = &self.integrations {
2798            if integrations.len() > limits.integration.count as usize {
2799                bail!(
2800                    "integration count {} greater than limit {}",
2801                    integrations.len(),
2802                    limits.integration.count
2803                );
2804            }
2805
2806            for integration in integrations {
2807                if let Some(timeout) = integration.timeout
2808                    && timeout > limits.integration.max_timeout
2809                {
2810                    bail!(
2811                        "integration timeout {} greater than limit {} for {}",
2812                        timeout,
2813                        limits.integration.max_timeout,
2814                        integration.name,
2815                    );
2816                }
2817            }
2818        }
2819
2820        if let Some(actions) = &self.actions {
2821            if actions.len() > limits.action.count as usize {
2822                bail!(
2823                    "action count {} greater than limit {}",
2824                    actions.len(),
2825                    limits.action.count
2826                );
2827            }
2828
2829            for action in actions {
2830                if action.privileged == Some(true) {
2831                    bail!("action {} is not under a privileged domain", action.name);
2832                }
2833
2834                if let Some(timeout) = action.timeout
2835                    && timeout > limits.action.max_timeout
2836                {
2837                    bail!(
2838                        "action timeout {} greater than limit {} for {}",
2839                        timeout,
2840                        limits.action.max_timeout,
2841                        action.name
2842                    );
2843                }
2844            }
2845        }
2846
2847        if let Some(storage_size) = self.storage_size
2848            && storage_size > limits.storage.max_app_storage
2849        {
2850            bail!("storage size is greater than limit");
2851        }
2852
2853        if let Some(content) = &self.content {
2854            if content.definitions.len() > limits.storage.content.max_content_definitions as usize {
2855                bail!(
2856                    "content definition count {} greater than limit {}",
2857                    content.definitions.len(),
2858                    limits.storage.content.max_content_definitions
2859                );
2860            }
2861
2862            for content_def in &content.definitions {
2863                if content_def.fields.len() > limits.storage.content.max_content_fields as usize {
2864                    bail!(
2865                        "content field count {} greater than limit {} for {}",
2866                        content_def.fields.len(),
2867                        limits.storage.content.max_content_fields,
2868                        content_def.name,
2869                    );
2870                }
2871
2872                for field in &content_def.fields {
2873                    if field.searchable == Some(true) && !limits.storage.content.search_enabled {
2874                        bail!(
2875                            "content field cannot be 'searchable' for field {} on definition {}",
2876                            field.name,
2877                            content_def.name,
2878                        );
2879                    }
2880                }
2881            }
2882        }
2883
2884        if let Some(models) = &self.models {
2885            if models.len() > limits.storage.model.max_model_definitions as usize {
2886                bail!(
2887                    "model definition count {} greater than limit {}",
2888                    models.len(),
2889                    limits.storage.model.max_model_definitions
2890                );
2891            }
2892
2893            for model in models {
2894                if model.fields.len() > limits.storage.model.max_model_fields as usize {
2895                    bail!(
2896                        "model field count {} greater than limit {} for {}",
2897                        model.fields.len(),
2898                        limits.storage.content.max_content_fields,
2899                        model.name,
2900                    );
2901                }
2902
2903                for field in &model.fields {
2904                    if field.searchable == Some(true) && !limits.storage.content.search_enabled {
2905                        bail!(
2906                            "content field cannot be 'searchable' for field {} on definition {}",
2907                            field.name,
2908                            model.name,
2909                        );
2910                    }
2911                }
2912            }
2913        }
2914
2915        if let Some(secrets) = &self.secrets
2916            && secrets.len() > limits.storage.secrets.max_count as usize
2917        {
2918            bail!(
2919                "secrets len {} exceeds max count limit {}",
2920                secrets.len(),
2921                limits.storage.secrets.max_count
2922            );
2923        }
2924
2925        Ok(())
2926    }
2927
2928    pub fn exec_lifecycle_script(
2929        proj_path: &Path,
2930        argument: &Option<String>,
2931        name: &str,
2932        when: &str,
2933        scripts: &Vec<Vec<String>>,
2934    ) -> anyhow::Result<()> {
2935        let span = tracing::info_span!("lifecycle", %when, %name);
2936
2937        span.in_scope(|| {
2938            let curr_dir = env::current_dir()?;
2939            env::set_current_dir(proj_path)?;
2940
2941            for script in scripts {
2942                let mut script_iter = script.iter();
2943
2944                if let Some(command) = script_iter.nth(0) {
2945                    let mut command_str = command.clone();
2946                    let mut command = Command::new(command);
2947
2948                    for arg in script_iter {
2949                        write!(command_str, " {arg}")?;
2950                        command.arg(arg);
2951                    }
2952
2953                    tracing::info!(cmd = %command_str, "exec");
2954
2955                    let output = match &argument {
2956                        Some(arg) => command.arg(arg).output()?,
2957                        None => command.output()?,
2958                    };
2959
2960                    if output.status.success() {
2961                        tracing::info!("success");
2962                    } else {
2963                        let stderr = str::from_utf8(&output.stderr)?;
2964                        let stdout = str::from_utf8(&output.stdout)?;
2965
2966                        tracing::error!(%stderr, %stdout, "failed");
2967                        bail!(stderr.to_string());
2968                    }
2969                }
2970            }
2971
2972            env::set_current_dir(curr_dir)?;
2973
2974            anyhow::Ok(())
2975        })
2976    }
2977}