1mod 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#[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
116macro_rules! ports {
129 ($($variant:ident),+ $(,)?) => {
130 impl Port {
131 pub const ALL: &'static [Port] = &[$(Port::$variant),+];
133 }
134
135 #[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
198pub struct Ports {
204 pub config: Arc<dyn Config>,
205 pub db: Option<Arc<dyn Database>>,
206 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 pub text_model: Option<Arc<dyn TextModel>>,
223 pub classifier: Option<Arc<dyn Classifier>>,
227 pub vector_index: Option<Arc<dyn VectorIndex>>,
230 pub embedder: Option<Arc<dyn Embedder>>,
233 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 pub dispatcher: Option<Arc<dyn Dispatcher>>,
244 pub tenants: Option<Arc<dyn crate::tenant::TenantRouting>>,
255}
256
257impl Ports {
258 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 #[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 #[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 #[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 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
427pub(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}