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