Skip to main content

ordinary_config/
lib.rs

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