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 auth;
9mod blob;
10mod captcha;
11mod classifier;
12mod clock;
13mod custom_hostnames;
14mod database;
15mod defer;
16mod dispatcher;
17mod embedder;
18mod http;
19mod idgen;
20mod kv;
21mod mailer;
22mod payments;
23mod push;
24mod rate_limiter;
25mod realtime;
26pub(crate) mod signer;
27mod text_model;
28mod tracker;
29mod vector_index;
30
31pub use auth::{Auth, AuthError, Caller, Subject, Unconfigured};
32pub use blob::{
33    Blob, BlobError, BlobObject, DEFAULT_PRESIGN_TTL, MAX_BLOB_BYTES, MAX_PRESIGN_TTL,
34    PresignedPut, ScopedBlob, check_blob_size,
35};
36pub use captcha::{Captcha, CaptchaBinding, CaptchaError, Verdict};
37pub use classifier::{
38    Answer, AnswerValue, Calibration, Classifier, ClassifierError, ClassifierProfile,
39    DEFAULT_MAX_STATE_CHARS, Question, validate_questions,
40};
41pub use clock::{Clock, SystemClock, timeout};
42pub use custom_hostnames::{
43    CertificateStatus, CustomHostname, CustomHostnameError, CustomHostnames, DnsRecordType,
44    HostnameClaim, HostnameRefusal, ProviderStatus, Validation, ValidationMethod, check_hostname,
45};
46pub use database::{Database, DbError, Row, Rows, Statement, TryFromValue};
47pub use defer::{Defer, NoopDefer};
48pub use dispatcher::{DispatchError, Dispatcher};
49pub use embedder::{EmbedError, Embedder, Embeddings};
50pub use http::{
51    BoundedHttpClient, DEFAULT_RESPONSE_TIMEOUT, HttpClient, HttpError, HttpPolicy,
52    MAX_CONCURRENT_REQUESTS, MAX_RESPONSE_BYTES, MAX_RESPONSE_TIMEOUT, declared_content_length,
53};
54pub use idgen::{IdGen, UlidIdGen};
55pub use kv::{KeyValue, KvError};
56pub use mailer::{MailError, Mailer, Message, SendOutcome};
57pub use payments::{
58    Charge, CheckoutRequest, CheckoutSession, ConnectAccountLink, ConnectAccountLinkRequest,
59    Dispute, DisputeListRequest, DisputePage, DisputePhase, DisputeStatus, LineItem, Money,
60    Payments, PaymentsError, PortalSession, PortalSessionRequest, Refund, RefundRequest,
61    SubscriptionCheckoutRequest, TransferCharge, UsageReport, UsageReported, WebhookEvent,
62};
63pub use push::{
64    LocKeys, Notification, Platform, Priority, Push, PushError, PushOutcome, Recipient,
65    RoutingPush, retry_after, ttl_secs,
66};
67pub use rate_limiter::{Decision, Quota, RateLimitError, RateLimiter};
68pub use realtime::{Member, Realtime, RealtimeError, RoomContext, RoomHandler};
69pub use signer::{Kid, MAX_KID_NAME, Payload, SignatureError, Signer};
70pub use text_model::{
71    Completion, DEFAULT_MAX_TOKENS, ModelTier, Prompt, Role, RoutingTextModel, TextModel,
72    TextModelError, Turn,
73};
74pub use tracker::{
75    Credential, Destination, Filed, InboundStatusError, RoutingTracker, Severity, StatusUpdate,
76    StatusWebhook, TicketComment, TicketDraft, TicketState, TicketStatus, Tracker, TrackerError,
77    receive_status,
78};
79pub use vector_index::{
80    ExactVectorIndex, MAX_NAMESPACE_BYTES, VectorFilter, VectorIndex, VectorIndexError,
81    VectorMatch, VectorNamespace, VectorRecord,
82};
83
84use crate::config::Config;
85use crate::module::Module;
86use std::sync::Arc;
87use tracing::warn;
88
89/// Every port a module can declare in `requires()` / `optional()`
90/// (architecture section 4).
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum Port {
93    Db,
94    Auth,
95    Mailer,
96    Captcha,
97    RateLimiter,
98    Signer,
99    KeyValue,
100    Blob,
101    Push,
102    Payments,
103    Tracker,
104    Realtime,
105    TextModel,
106    Classifier,
107    VectorIndex,
108    Embedder,
109    CustomHostnames,
110    HttpClient,
111    Clock,
112    IdGen,
113    Defer,
114}
115
116/// Declares [`Port::ALL`] and, from the same list, a match that has to be
117/// exhaustive.
118///
119/// `ALL` is walked by [`Ports::provides`], so a variant missing from it
120/// is a port no bundle ever reports — which is exactly the bug that left
121/// `Port::Auth` out of `provides` when that was fifteen hand-written
122/// branches. Writing the list twice is what made it possible; writing it
123/// once is what stops it.
124///
125/// `#[non_exhaustive]` does not make an in-crate match non-exhaustive, so
126/// the check is real here even though a downstream `match` on `Port`
127/// needs a wildcard.
128macro_rules! ports {
129    ($($variant:ident),+ $(,)?) => {
130        impl Port {
131            /// Every port.
132            pub const ALL: &'static [Port] = &[$(Port::$variant),+];
133        }
134
135        /// Never called. It exists so that a variant absent from the list
136        /// above is a compile error here.
137        #[expect(dead_code, reason = "its only job is to be exhaustive")]
138        fn every_port_is_in_all(port: Port) {
139            match port {
140                $(Port::$variant => {}),+
141            }
142        }
143    };
144}
145
146ports!(
147    Db,
148    Auth,
149    Mailer,
150    Captcha,
151    RateLimiter,
152    Signer,
153    KeyValue,
154    Blob,
155    Push,
156    Payments,
157    Tracker,
158    Realtime,
159    TextModel,
160    Classifier,
161    VectorIndex,
162    Embedder,
163    CustomHostnames,
164    HttpClient,
165    Clock,
166    IdGen,
167    Defer,
168);
169
170impl Port {
171    pub fn name(&self) -> &'static str {
172        match self {
173            Port::Db => "Database",
174            Port::Auth => "Auth",
175            Port::Mailer => "Mailer",
176            Port::Captcha => "Captcha",
177            Port::RateLimiter => "RateLimiter",
178            Port::Signer => "Signer",
179            Port::KeyValue => "KeyValue",
180            Port::Blob => "Blob",
181            Port::Push => "Push",
182            Port::Payments => "Payments",
183            Port::Tracker => "Tracker",
184            Port::Realtime => "Realtime",
185            Port::TextModel => "TextModel",
186            Port::Classifier => "Classifier",
187            Port::VectorIndex => "vector_index",
188            Port::Embedder => "embedder",
189            Port::CustomHostnames => "custom_hostnames",
190            Port::HttpClient => "HttpClient",
191            Port::Clock => "Clock",
192            Port::IdGen => "IdGen",
193            Port::Defer => "Defer",
194        }
195    }
196}
197
198/// The per-request bundle of resolved port implementations plus the typed
199/// config the runtime built from environment/secrets.
200///
201/// Every port is optional: the Cloudflare runtime resolves what the venture's
202/// bindings actually provide and leaves the rest `None`.
203pub struct Ports {
204    pub config: Arc<dyn Config>,
205    pub db: Option<Arc<dyn Database>>,
206    /// Who a request's credentials speak for (issue #153). `None` is a
207    /// deployment that cannot identify a caller at all; a module needing
208    /// one declares [`Port::Auth`] and is refused composition here.
209    pub auth: Option<Arc<dyn Auth>>,
210    pub mailer: Option<Arc<dyn Mailer>>,
211    pub captcha: Option<Arc<dyn Captcha>>,
212    pub rate_limiter: Option<Arc<dyn RateLimiter>>,
213    pub signer: Option<Arc<dyn Signer>>,
214    pub kv: Option<Arc<dyn KeyValue>>,
215    pub blob: Option<Arc<dyn Blob>>,
216    pub push: Option<Arc<dyn Push>>,
217    pub payments: Option<Arc<dyn Payments>>,
218    pub tracker: Option<Arc<dyn Tracker>>,
219    pub realtime: Option<Arc<dyn Realtime>>,
220    /// A text completion by [`ModelTier`](crate::ModelTier), never by
221    /// vendor (issue #429).
222    pub text_model: Option<Arc<dyn TextModel>>,
223    /// A typed, calibrated decision by a set of
224    /// [`Question`](crate::Question)s, never by vendor (issue #456) —
225    /// the sibling of `text_model`.
226    pub classifier: Option<Arc<dyn Classifier>>,
227    /// Nearest-neighbour search over tenant-scoped namespaces (issue
228    /// #561), the output side of embeddings, as `text_model` is.
229    pub vector_index: Option<Arc<dyn VectorIndex>>,
230    /// Text into embedding vectors (issue #561), the input side of
231    /// [`Ports::vector_index`].
232    pub embedder: Option<Arc<dyn Embedder>>,
233    /// Custom hostnames a customer owns, served by a venture (issue
234    /// #590) — Cloudflare for `SaaS` behind the same trait on every
235    /// runtime.
236    pub custom_hostnames: Option<Arc<dyn CustomHostnames>>,
237    pub http: Option<Arc<dyn HttpClient>>,
238    pub clock: Option<Arc<dyn Clock>>,
239    pub id_gen: Option<Arc<dyn IdGen>>,
240    pub defer: Option<Arc<dyn Defer>>,
241    /// Set by the runtime when the venture mounts sidecar modules. Not a
242    /// [`Port`], so `view_for` never copies it and no module can reach it.
243    pub dispatcher: Option<Arc<dyn Dispatcher>>,
244    /// How this deployment turns a host into a tenant, and a tenant into
245    /// its database (TENANT-ROUTING.md §3-§5). `None` is the *no registry*
246    /// deployment — Cloudflare, the browser, and native in development and
247    /// test — which resolves every host to the implicit tenant and hands
248    /// back [`Ports::db`].
249    ///
250    /// Not a [`Port`] for the same reason `dispatcher` is not: `view_for`
251    /// must never copy it into a module's view. A module reaches a
252    /// database through the `TenantConn` extractor, which can only give it
253    /// the one the request resolved.
254    pub tenants: Option<Arc<dyn crate::tenant::TenantRouting>>,
255}
256
257impl Ports {
258    /// An empty bundle with no ports resolved and an
259    /// [`EmptyConfig`](crate::config::EmptyConfig).
260    pub fn empty() -> Self {
261        Self::with_config(Arc::new(crate::config::EmptyConfig))
262    }
263
264    pub fn with_config(config: Arc<dyn Config>) -> Self {
265        Self {
266            config,
267            db: None,
268            auth: None,
269            mailer: None,
270            captcha: None,
271            rate_limiter: None,
272            signer: None,
273            kv: None,
274            blob: None,
275            push: None,
276            payments: None,
277            tracker: None,
278            realtime: None,
279            text_model: None,
280            classifier: None,
281            vector_index: None,
282            embedder: None,
283            custom_hostnames: None,
284            http: None,
285            clock: None,
286            id_gen: None,
287            defer: None,
288            dispatcher: None,
289            tenants: None,
290        }
291    }
292
293    /// The set of ports this bundle actually provides.
294    ///
295    /// Walks [`Port::ALL`] and asks [`Self::has`], so a variant added to
296    /// the enum cannot be left out of the answer: the match in `has` is
297    /// exhaustive and the compiler refuses a new one until it is
298    /// handled. Written out as fifteen `if`s, it had already lost one —
299    /// `Port::Auth` was missing, so this reported fourteen of fifteen
300    /// and a bundle with a verifier wired said it had none.
301    #[must_use]
302    pub fn provides(&self) -> Vec<Port> {
303        Port::ALL
304            .iter()
305            .copied()
306            .filter(|port| self.has(*port))
307            .collect()
308    }
309
310    /// Whether one port is wired in this bundle.
311    ///
312    /// The exhaustive match is the point. `dispatcher` and `tenants` are
313    /// deliberately absent from [`Port`] — no module may reach either —
314    /// so they have nothing to answer here.
315    #[must_use]
316    pub fn has(&self, port: Port) -> bool {
317        match port {
318            Port::Db => self.db.is_some(),
319            Port::Auth => self.auth.is_some(),
320            Port::Mailer => self.mailer.is_some(),
321            Port::Captcha => self.captcha.is_some(),
322            Port::RateLimiter => self.rate_limiter.is_some(),
323            Port::Signer => self.signer.is_some(),
324            Port::KeyValue => self.kv.is_some(),
325            Port::Blob => self.blob.is_some(),
326            Port::Push => self.push.is_some(),
327            Port::Payments => self.payments.is_some(),
328            Port::Tracker => self.tracker.is_some(),
329            Port::Realtime => self.realtime.is_some(),
330            Port::TextModel => self.text_model.is_some(),
331            Port::Classifier => self.classifier.is_some(),
332            Port::VectorIndex => self.vector_index.is_some(),
333            Port::Embedder => self.embedder.is_some(),
334            Port::CustomHostnames => self.custom_hostnames.is_some(),
335            Port::HttpClient => self.http.is_some(),
336            Port::Clock => self.clock.is_some(),
337            Port::IdGen => self.id_gen.is_some(),
338            Port::Defer => self.defer.is_some(),
339        }
340    }
341
342    /// A copy of this bundle in which every port the module did not declare
343    /// in `requires()` or `optional()` is `None`, so a module cannot use
344    /// what it did not declare (issue #3). Undeclared-but-provided ports are
345    /// logged once per module by `Harness::build`.
346    #[must_use]
347    pub fn view_for(&self, module: &dyn Module) -> Self {
348        let declared = module
349            .requires()
350            .iter()
351            .chain(module.optional())
352            .copied()
353            .collect::<Vec<_>>();
354        let allows = |p: &[Port], port: Port| p.contains(&port);
355        let mut view = Ports::with_config(self.config.clone());
356        if allows(&declared, Port::Db) {
357            view.db.clone_from(&self.db);
358        }
359        if allows(&declared, Port::Auth) {
360            view.auth.clone_from(&self.auth);
361        }
362        if allows(&declared, Port::Mailer) {
363            view.mailer.clone_from(&self.mailer);
364        }
365        if allows(&declared, Port::Captcha) {
366            view.captcha.clone_from(&self.captcha);
367        }
368        if allows(&declared, Port::RateLimiter) {
369            view.rate_limiter.clone_from(&self.rate_limiter);
370        }
371        if allows(&declared, Port::Signer) {
372            view.signer.clone_from(&self.signer);
373        }
374        if allows(&declared, Port::KeyValue) {
375            view.kv.clone_from(&self.kv);
376        }
377        if allows(&declared, Port::Blob) {
378            // Scope the store to this module's prefix, the blob equivalent of
379            // the table-ownership rule: a module cannot name another's objects.
380            view.blob = self.blob.as_ref().map(|blob| {
381                Arc::new(ScopedBlob::new(Arc::clone(blob), module.name())) as Arc<dyn Blob>
382            });
383        }
384        if allows(&declared, Port::Push) {
385            view.push.clone_from(&self.push);
386        }
387        if allows(&declared, Port::Payments) {
388            view.payments.clone_from(&self.payments);
389        }
390        if allows(&declared, Port::Tracker) {
391            view.tracker.clone_from(&self.tracker);
392        }
393        if allows(&declared, Port::Realtime) {
394            view.realtime.clone_from(&self.realtime);
395        }
396        if allows(&declared, Port::TextModel) {
397            view.text_model.clone_from(&self.text_model);
398        }
399        if allows(&declared, Port::Classifier) {
400            view.classifier.clone_from(&self.classifier);
401        }
402        if allows(&declared, Port::VectorIndex) {
403            view.vector_index.clone_from(&self.vector_index);
404        }
405        if allows(&declared, Port::Embedder) {
406            view.embedder.clone_from(&self.embedder);
407        }
408        if allows(&declared, Port::CustomHostnames) {
409            view.custom_hostnames.clone_from(&self.custom_hostnames);
410        }
411        if allows(&declared, Port::HttpClient) {
412            view.http.clone_from(&self.http);
413        }
414        if allows(&declared, Port::Clock) {
415            view.clock.clone_from(&self.clock);
416        }
417        if allows(&declared, Port::IdGen) {
418            view.id_gen.clone_from(&self.id_gen);
419        }
420        if allows(&declared, Port::Defer) {
421            view.defer.clone_from(&self.defer);
422        }
423        view
424    }
425}
426
427/// Log (once, at `Harness::build`) the provided ports a module did not
428/// declare — the ports `view_for` will hide from it.
429pub(crate) fn warn_undeclared_ports(module: &dyn Module, provided: &[Port]) {
430    for port in provided {
431        if !module.requires().contains(port) && !module.optional().contains(port) {
432            warn!(
433                module = module.name(),
434                port = port.name(),
435                "runtime provides a port the module did not declare; hiding it",
436            );
437        }
438    }
439}