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