Skip to main content

cratefield_core/ports/
mod.rs

1//! Port traits (ADR 0002, architecture section 5). Modules see these traits
2//! and nothing else — never a Cloudflare binding, never a vendor client.
3//!
4//! All traits are `Send + Sync` and object-safe, used as `Arc<dyn Trait>`.
5//! Async methods use `async_trait` until native `async fn` in traits is
6//! ergonomic for trait objects.
7
8mod blob;
9mod captcha;
10mod clock;
11mod database;
12mod defer;
13mod dispatcher;
14mod http;
15mod idgen;
16mod kv;
17mod mailer;
18mod payments;
19mod push;
20mod rate_limiter;
21mod realtime;
22pub(crate) mod signer;
23
24pub use blob::{Blob, BlobError, BlobObject, MAX_BLOB_BYTES, ScopedBlob, check_blob_size};
25pub use captcha::{Captcha, CaptchaBinding, CaptchaError, Verdict};
26pub use clock::{Clock, SystemClock, timeout};
27pub use database::{Database, DbError, Row, Rows, Statement, TryFromValue};
28pub use defer::{Defer, NoopDefer};
29pub use dispatcher::{DispatchError, Dispatcher};
30pub use http::{
31    BoundedHttpClient, DEFAULT_RESPONSE_TIMEOUT, HttpClient, HttpError, HttpPolicy,
32    MAX_CONCURRENT_REQUESTS, MAX_RESPONSE_BYTES, MAX_RESPONSE_TIMEOUT, declared_content_length,
33};
34pub use idgen::{IdGen, UlidIdGen};
35pub use kv::{KeyValue, KvError};
36pub use mailer::{MailError, Mailer, Message, SendOutcome};
37pub use payments::{
38    Charge, CheckoutRequest, CheckoutSession, ConnectAccountLink, ConnectAccountLinkRequest,
39    LineItem, Money, Payments, PaymentsError, Refund, RefundRequest, SubscriptionCheckoutRequest,
40    TransferCharge, WebhookEvent,
41};
42pub use push::{
43    LocKeys, Notification, Platform, Priority, Push, PushError, PushOutcome, Recipient,
44    RoutingPush, retry_after, ttl_secs,
45};
46pub use rate_limiter::{Decision, RateLimitError, RateLimiter};
47pub use realtime::{Member, Realtime, RealtimeError, RoomContext, RoomHandler};
48pub use signer::{Kid, MAX_KID_NAME, Payload, SignatureError, Signer};
49
50use crate::config::Config;
51use crate::module::Module;
52use std::sync::Arc;
53use tracing::warn;
54
55/// Every port a module can declare in `requires()` / `optional()`
56/// (architecture section 4).
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum Port {
59    Db,
60    Mailer,
61    Captcha,
62    RateLimiter,
63    Signer,
64    KeyValue,
65    Blob,
66    Push,
67    Payments,
68    Realtime,
69    HttpClient,
70    Clock,
71    IdGen,
72    Defer,
73}
74
75impl Port {
76    pub const ALL: &'static [Port] = &[
77        Port::Db,
78        Port::Mailer,
79        Port::Captcha,
80        Port::RateLimiter,
81        Port::Signer,
82        Port::KeyValue,
83        Port::Blob,
84        Port::Push,
85        Port::Payments,
86        Port::Realtime,
87        Port::HttpClient,
88        Port::Clock,
89        Port::IdGen,
90        Port::Defer,
91    ];
92
93    pub fn name(&self) -> &'static str {
94        match self {
95            Port::Db => "Database",
96            Port::Mailer => "Mailer",
97            Port::Captcha => "Captcha",
98            Port::RateLimiter => "RateLimiter",
99            Port::Signer => "Signer",
100            Port::KeyValue => "KeyValue",
101            Port::Blob => "Blob",
102            Port::Push => "Push",
103            Port::Payments => "Payments",
104            Port::Realtime => "Realtime",
105            Port::HttpClient => "HttpClient",
106            Port::Clock => "Clock",
107            Port::IdGen => "IdGen",
108            Port::Defer => "Defer",
109        }
110    }
111}
112
113/// The per-request bundle of resolved port implementations plus the typed
114/// config the runtime built from environment/secrets.
115///
116/// Every port is optional: the Cloudflare runtime resolves what the venture's
117/// bindings actually provide and leaves the rest `None`.
118pub struct Ports {
119    pub config: Arc<dyn Config>,
120    pub db: Option<Arc<dyn Database>>,
121    pub mailer: Option<Arc<dyn Mailer>>,
122    pub captcha: Option<Arc<dyn Captcha>>,
123    pub rate_limiter: Option<Arc<dyn RateLimiter>>,
124    pub signer: Option<Arc<dyn Signer>>,
125    pub kv: Option<Arc<dyn KeyValue>>,
126    pub blob: Option<Arc<dyn Blob>>,
127    pub push: Option<Arc<dyn Push>>,
128    pub payments: Option<Arc<dyn Payments>>,
129    pub realtime: Option<Arc<dyn Realtime>>,
130    pub http: Option<Arc<dyn HttpClient>>,
131    pub clock: Option<Arc<dyn Clock>>,
132    pub id_gen: Option<Arc<dyn IdGen>>,
133    pub defer: Option<Arc<dyn Defer>>,
134    /// Set by the runtime when the venture mounts sidecar modules. Not a
135    /// [`Port`], so `view_for` never copies it and no module can reach it.
136    pub dispatcher: Option<Arc<dyn Dispatcher>>,
137    /// How this deployment turns a host into a tenant, and a tenant into
138    /// its database (TENANT-ROUTING.md §3-§5). `None` is the *no registry*
139    /// deployment — Cloudflare, the browser, and native in development and
140    /// test — which resolves every host to the implicit tenant and hands
141    /// back [`Ports::db`].
142    ///
143    /// Not a [`Port`] for the same reason `dispatcher` is not: `view_for`
144    /// must never copy it into a module's view. A module reaches a
145    /// database through the `TenantConn` extractor, which can only give it
146    /// the one the request resolved.
147    pub tenants: Option<Arc<dyn crate::tenant::TenantRouting>>,
148}
149
150impl Ports {
151    /// An empty bundle with no ports resolved and an
152    /// [`EmptyConfig`](crate::config::EmptyConfig).
153    pub fn empty() -> Self {
154        Self::with_config(Arc::new(crate::config::EmptyConfig))
155    }
156
157    pub fn with_config(config: Arc<dyn Config>) -> Self {
158        Self {
159            config,
160            db: None,
161            mailer: None,
162            captcha: None,
163            rate_limiter: None,
164            signer: None,
165            kv: None,
166            blob: None,
167            push: None,
168            payments: None,
169            realtime: None,
170            http: None,
171            clock: None,
172            id_gen: None,
173            defer: None,
174            dispatcher: None,
175            tenants: None,
176        }
177    }
178
179    /// The set of ports this bundle actually provides.
180    pub fn provides(&self) -> Vec<Port> {
181        let mut provided = Vec::new();
182        if self.db.is_some() {
183            provided.push(Port::Db);
184        }
185        if self.mailer.is_some() {
186            provided.push(Port::Mailer);
187        }
188        if self.captcha.is_some() {
189            provided.push(Port::Captcha);
190        }
191        if self.rate_limiter.is_some() {
192            provided.push(Port::RateLimiter);
193        }
194        if self.signer.is_some() {
195            provided.push(Port::Signer);
196        }
197        if self.kv.is_some() {
198            provided.push(Port::KeyValue);
199        }
200        if self.blob.is_some() {
201            provided.push(Port::Blob);
202        }
203        if self.push.is_some() {
204            provided.push(Port::Push);
205        }
206        if self.payments.is_some() {
207            provided.push(Port::Payments);
208        }
209        if self.realtime.is_some() {
210            provided.push(Port::Realtime);
211        }
212        if self.http.is_some() {
213            provided.push(Port::HttpClient);
214        }
215        if self.clock.is_some() {
216            provided.push(Port::Clock);
217        }
218        if self.id_gen.is_some() {
219            provided.push(Port::IdGen);
220        }
221        if self.defer.is_some() {
222            provided.push(Port::Defer);
223        }
224        provided
225    }
226
227    /// A copy of this bundle in which every port the module did not declare
228    /// in `requires()` or `optional()` is `None`, so a module cannot use
229    /// what it did not declare (issue #3). Undeclared-but-provided ports are
230    /// logged once per module by `Harness::build`.
231    #[must_use]
232    pub fn view_for(&self, module: &dyn Module) -> Self {
233        let declared = module
234            .requires()
235            .iter()
236            .chain(module.optional())
237            .copied()
238            .collect::<Vec<_>>();
239        let allows = |p: &[Port], port: Port| p.contains(&port);
240        let mut view = Ports::with_config(self.config.clone());
241        if allows(&declared, Port::Db) {
242            view.db.clone_from(&self.db);
243        }
244        if allows(&declared, Port::Mailer) {
245            view.mailer.clone_from(&self.mailer);
246        }
247        if allows(&declared, Port::Captcha) {
248            view.captcha.clone_from(&self.captcha);
249        }
250        if allows(&declared, Port::RateLimiter) {
251            view.rate_limiter.clone_from(&self.rate_limiter);
252        }
253        if allows(&declared, Port::Signer) {
254            view.signer.clone_from(&self.signer);
255        }
256        if allows(&declared, Port::KeyValue) {
257            view.kv.clone_from(&self.kv);
258        }
259        if allows(&declared, Port::Blob) {
260            // Scope the store to this module's prefix, the blob equivalent of
261            // the table-ownership rule: a module cannot name another's objects.
262            view.blob = self.blob.as_ref().map(|blob| {
263                Arc::new(ScopedBlob::new(Arc::clone(blob), module.name())) as Arc<dyn Blob>
264            });
265        }
266        if allows(&declared, Port::Push) {
267            view.push.clone_from(&self.push);
268        }
269        if allows(&declared, Port::Payments) {
270            view.payments.clone_from(&self.payments);
271        }
272        if allows(&declared, Port::Realtime) {
273            view.realtime.clone_from(&self.realtime);
274        }
275        if allows(&declared, Port::HttpClient) {
276            view.http.clone_from(&self.http);
277        }
278        if allows(&declared, Port::Clock) {
279            view.clock.clone_from(&self.clock);
280        }
281        if allows(&declared, Port::IdGen) {
282            view.id_gen.clone_from(&self.id_gen);
283        }
284        if allows(&declared, Port::Defer) {
285            view.defer.clone_from(&self.defer);
286        }
287        view
288    }
289}
290
291/// Log (once, at `Harness::build`) the provided ports a module did not
292/// declare — the ports `view_for` will hide from it.
293pub(crate) fn warn_undeclared_ports(module: &dyn Module, provided: &[Port]) {
294    for port in provided {
295        if !module.requires().contains(port) && !module.optional().contains(port) {
296            warn!(
297                module = module.name(),
298                port = port.name(),
299                "runtime provides a port the module did not declare; hiding it",
300            );
301        }
302    }
303}