Skip to main content

fn0_shared_schema/
lib.rs

1use forte_macros::forte_doc;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5pub use doc_db::DbRequest;
6
7#[derive(Serialize, Deserialize, Clone)]
8pub struct WorkerProjectManifest {
9    pub code_version: u64,
10    /// The project's registered domain. A project answers on it and nothing
11    /// else, so an entry without one cannot receive a request; the worker
12    /// serves nothing for an empty value.
13    ///
14    /// Deserialization tolerates the pre-rename shape (`custom_domain`, and
15    /// `null` from projects that never registered one) so a manifest written
16    /// before this field was required does not poison the whole document.
17    /// New control writes only `domain`, so roll the worker fleet out before
18    /// control: an old worker cannot read a manifest that new control wrote.
19    #[serde(
20        alias = "custom_domain",
21        default,
22        deserialize_with = "deserialize_domain"
23    )]
24    pub domain: String,
25    #[serde(default = "default_static_cache_state")]
26    pub static_cache_state: String,
27    #[serde(default)]
28    pub pending_code_version: Option<u64>,
29    /// Absent only between a project's creation and its owner connecting a
30    /// Cloudflare account; a worker cannot serve the project until it is set.
31    #[serde(default)]
32    pub storage: Option<WorkerProjectStorage>,
33}
34
35/// One R2 token as it travels to the worker: the key id in the clear, the
36/// secret only as KMS ciphertext, so a leaked manifest row is not a usable
37/// credential.
38#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
39pub struct WorkerR2Credential {
40    pub access_key_id: String,
41    pub secret_ciphertext: String,
42}
43
44/// Where one project's objects live, as the worker sees it.
45///
46/// One credential, scoped to exactly the three buckets named here. The
47/// frontend-asset bucket is deliberately outside it: nothing in the worker
48/// serves assets — the CDN does, straight off the bucket — so a fleet-wide
49/// credential able to rewrite a deployed frontend would be reach with no use
50/// for it.
51#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
52pub struct WorkerProjectStorage {
53    pub account_id: String,
54    pub region: String,
55    pub credential: WorkerR2Credential,
56    pub private_object_storage_bucket: String,
57    pub public_object_storage_bucket: String,
58    /// CDN origin for `public_object_storage_bucket`, without a trailing slash.
59    pub public_object_storage_base_url: String,
60    /// Bumped by control on every credential or bucket change, so a worker can
61    /// skip re-decrypting a target it already holds.
62    pub config_version: u64,
63}
64
65pub const STATIC_CACHE_STATE_ACTIVE: &str = "active";
66pub const STATIC_CACHE_STATE_PRE_PURGE: &str = "pre_purge";
67pub const STATIC_CACHE_STATE_ACTIVATING: &str = "activating";
68
69fn default_static_cache_state() -> String {
70    STATIC_CACHE_STATE_ACTIVE.to_string()
71}
72
73fn deserialize_domain<'de, D>(deserializer: D) -> Result<String, D::Error>
74where
75    D: serde::Deserializer<'de>,
76{
77    struct DomainVisitor;
78    impl<'de> serde::de::Visitor<'de> for DomainVisitor {
79        type Value = String;
80
81        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
82            formatter.write_str("a string or null")
83        }
84
85        fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<String, E> {
86            Ok(value.to_string())
87        }
88
89        fn visit_none<E: serde::de::Error>(self) -> Result<String, E> {
90            Ok(String::new())
91        }
92
93        fn visit_some<D: serde::Deserializer<'de>>(
94            self,
95            deserializer: D,
96        ) -> Result<String, D::Error> {
97            Deserialize::deserialize(deserializer)
98        }
99    }
100    deserializer.deserialize_option(DomainVisitor)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::WorkerProjectManifest;
106
107    #[test]
108    fn domain_reads_the_legacy_custom_domain_key() {
109        let entry: WorkerProjectManifest =
110            serde_json::from_str(r#"{"code_version":0,"custom_domain":"app.example.com"}"#)
111                .unwrap();
112        assert_eq!(entry.domain, "app.example.com");
113    }
114
115    #[test]
116    fn null_legacy_domain_reads_as_empty() {
117        let entry: WorkerProjectManifest =
118            serde_json::from_str(r#"{"code_version":0,"custom_domain":null}"#).unwrap();
119        assert_eq!(entry.domain, "");
120    }
121
122    #[test]
123    fn domain_serializes_under_its_own_name() {
124        let entry: WorkerProjectManifest =
125            serde_json::from_str(r#"{"code_version":0,"domain":"app.example.com"}"#).unwrap();
126        let json = serde_json::to_string(&entry).unwrap();
127        assert!(json.contains(r#""domain":"app.example.com""#));
128        assert!(!json.contains("custom_domain"));
129    }
130}
131
132#[forte_doc]
133pub struct WorkerManifestDoc {
134    pub manifest_version: u64,
135    pub project_manifests: HashMap<String, WorkerProjectManifest>,
136}
137
138/// A certificate the worker serves for one custom hostname, issued through the
139/// project owner's own Cloudflare Origin CA. Only valid for the Cloudflare edge
140/// to origin leg, which is the only leg the worker terminates.
141#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
142pub struct WorkerHostnameCert {
143    pub project_id: String,
144    pub cert_pem: String,
145    pub key_ciphertext: String,
146    pub not_after_epoch_seconds: i64,
147}
148
149/// Certificates live outside `WorkerManifestDoc` because every worker polls
150/// that document once a second and a PEM per project is a different order of
151/// magnitude from a bucket name. Custom hostnames are far fewer than projects.
152#[forte_doc]
153pub struct WorkerCertManifestDoc {
154    pub cert_version: u64,
155    pub certs: HashMap<String, WorkerHostnameCert>,
156}
157
158#[forte_doc]
159pub struct WorkerHostStatusDoc {
160    #[sk]
161    pub host_id: String,
162    pub addr: String,
163    pub active_image_ref: Option<String>,
164    pub reported_at: i64,
165}
166
167#[forte_doc]
168pub struct WebSocketConnectionDoc {
169    #[sk]
170    pub connection_id: String,
171    pub project_id: String,
172    pub worker_id: String,
173    pub endpoint: String,
174}
175
176#[forte_doc]
177pub struct WebSocketDirectoryGcCursorDoc {
178    pub after_connection_id: Option<String>,
179}