1use std::collections::BTreeSet;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::time::Duration;
14
15use axum::Router;
16use axum::extract::{DefaultBodyLimit, State};
17use axum::http::{HeaderMap, StatusCode, header};
18use axum::middleware::{Next, from_fn_with_state};
19use axum::response::{IntoResponse, Response};
20use axum::routing::{get, post};
21use bytes::Bytes;
22use serde_json::json;
23use tracing::error;
24
25use crate::admin::require_admin;
26use crate::config::Config;
27use crate::config::ConfigError;
28use crate::events::EventBus;
29use crate::http::{
30 Json, MAX_BODY_BYTES, ScopeState, cors_layer, rate_limited, scope_layer,
31 security_headers_layer, token_response_layer,
32};
33use crate::module::{HARNESS_API, Module, ModuleContext, harness_api_mismatch};
34use crate::ports::Dispatcher;
35use crate::ports::{
36 Clock, Database, Port, Ports, RateLimiter, Statement, SystemClock, warn_undeclared_ports,
37};
38use crate::problem::Problem;
39use crate::problems::SLUGS;
40use crate::rate_limit::{RateLimit, RateLimitFailure};
41use crate::route_policy::deployed_env;
42use crate::scheduled::ScheduledBudget;
43use crate::scope::Scope;
44use crate::sidecar::{
45 GatewayGuard, SIDECAR_REQUIRE_GATEWAY, SidecarMount, X_HARNESS_GATEWAY, gateway_guard,
46 gateway_signer, mint_gateway, truthy,
47};
48use crate::signer::HmacSigner;
49use crate::surface::{
50 MAX_SIDECAR_SURFACE_BYTES, RenderedSurface, SurfaceDocument, SurfaceSource, UiContext, UiMount,
51 sanitize_sidecar_document, strip_unguarded_captcha_actions,
52};
53use crate::template::{Template, TemplateRegistry};
54use crate::venture::{Venture, VentureEnv};
55
56fn check_tables(
65 module: &dyn Module,
66 tables: &mut HashMap<&'static str, &'static str>,
67 errors: &mut ConfigError,
68) {
69 let name = module.name();
70 for table in module.tables() {
71 match tables.get(table) {
72 Some(owner) => errors.push(format!(
73 "duplicate table `{table}` claimed by modules `{owner}` and `{name}`"
74 )),
75 None => {
76 tables.insert(table, name);
77 }
78 }
79 }
80
81 let owns = module.tables();
85 let mut declared: Vec<&'static str> = Vec::new();
86 for set in module.personal_data() {
87 for problem in set.validate(name, owns) {
88 errors.push(problem);
89 }
90 if declared.contains(&set.table) {
91 errors.push(format!(
92 "module `{name}` declares table `{}` twice in personal_data()",
93 set.table
94 ));
95 }
96 declared.push(set.table);
97 }
98}
99
100pub trait Runtime: Send + Sync + 'static {
105 fn provides(&self) -> Vec<Port>;
106 fn effectively_configured(&self, port: Port) -> bool {
112 self.provides().contains(&port)
113 }
114}
115
116pub struct Harness {
118 venture: Arc<Venture>,
119 modules: Vec<Arc<dyn Module>>,
120 templates: Arc<TemplateRegistry>,
121 events: EventBus,
122 runtime: Option<Arc<dyn Runtime>>,
123 well_known: Option<Router>,
126 surface: Arc<SurfaceVariants>,
129 ui: Option<Arc<dyn UiMount>>,
131 personal_data: Arc<crate::PersonalDataCatalog>,
134 unprotected_acceptance_recorded: AtomicBool,
144 unlimited_acceptance_recorded: AtomicBool,
145}
146
147struct SurfaceVariants {
148 document: Arc<SurfaceDocument>,
149 full: RenderedSurface,
150 public: RenderedSurface,
151}
152
153impl SurfaceVariants {
154 fn compose(
155 venture: &Venture,
156 modules: &[Arc<dyn Module>],
157 ui: Option<&Arc<dyn UiMount>>,
158 ) -> Self {
159 let mut document = SurfaceDocument::compose(venture, modules);
160 document.ui = ui.and_then(|ui| ui.describe());
161 Self {
162 full: RenderedSurface::render(&document),
163 public: RenderedSurface::render(&document.public()),
164 document: Arc::new(document),
165 }
166 }
167}
168
169impl std::fmt::Debug for Harness {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 let modules: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
172 f.debug_struct("Harness")
173 .field("venture", &self.venture.name)
174 .field("modules", &modules)
175 .finish_non_exhaustive()
176 }
177}
178
179impl Harness {
180 #[must_use]
184 pub fn clone_arc(self: &std::sync::Arc<Self>) -> std::sync::Arc<Self> {
185 std::sync::Arc::clone(self)
186 }
187 pub fn builder() -> HarnessBuilder {
188 HarnessBuilder::default()
189 }
190
191 pub fn venture(&self) -> &Arc<Venture> {
192 &self.venture
193 }
194
195 pub fn modules(&self) -> &[Arc<dyn Module>] {
196 &self.modules
197 }
198
199 pub fn max_body_bytes(&self, path: &str, cfg: &dyn Config) -> usize {
222 let path = match path.split_once(['?', '#']) {
225 Some((path, _)) => path,
226 None => path,
227 };
228 let mut segments = path.split('/').filter(|segment| !segment.is_empty());
229 if segments.next() != Some("v1") {
230 return MAX_BODY_BYTES;
231 }
232 let Some(name) = segments.next() else {
233 return MAX_BODY_BYTES;
234 };
235 self.modules
236 .iter()
237 .find(|module| module.name() == name)
238 .map_or(MAX_BODY_BYTES, |module| {
239 MAX_BODY_BYTES.max(module.max_body_bytes(cfg))
240 })
241 }
242
243 pub fn templates(&self) -> &Arc<TemplateRegistry> {
244 &self.templates
245 }
246
247 pub fn events(&self) -> &EventBus {
248 &self.events
249 }
250
251 pub fn module_context(&self, module: &dyn Module, ports: &Ports) -> ModuleContext {
256 let venture = self.venture_as_deployed(ports.config.as_ref());
271 ModuleContext {
272 config: Arc::clone(&ports.config),
273 ports: ports.view_for(module),
274 events: self.events_for(ports),
275 templates: Arc::clone(&self.templates),
276 venture,
277 unprotected_writes_accepted: crate::route_policy::unprotected_writes_override(
278 ports.config.as_ref(),
279 )
280 .is_some(),
281 personal_data: Arc::clone(&self.personal_data),
282 ui_mounted: self.ui.is_some(),
283 scheduled: Arc::new(ScheduledBudget::unbounded()),
287 }
288 }
289
290 fn gateway_guard_state(ports: &Ports, gateway: Option<&Arc<HmacSigner>>) -> GatewayGuard {
300 GatewayGuard {
301 require: truthy(ports.config.as_ref(), SIDECAR_REQUIRE_GATEWAY),
302 signer: gateway.map(Arc::clone),
303 admin_token: ports
304 .config
305 .get("ADMIN_TOKEN")
306 .filter(|token| !token.is_empty()),
307 }
308 }
309
310 fn merged_surface(
313 &self,
314 mounts: Vec<SidecarMount>,
315 ports: &Ports,
316 gateway: Option<Arc<HmacSigner>>,
317 env: VentureEnv,
318 ) -> Arc<dyn SurfaceSource> {
319 Arc::new(MergedSurface {
320 base: Arc::clone(&self.surface),
321 mounts,
322 dispatcher: ports.dispatcher.clone(),
323 gateway,
324 env,
325 captcha_configured: ports.captcha.is_some(),
326 })
327 }
328
329 fn venture_as_deployed(&self, config: &dyn Config) -> Arc<Venture> {
335 let env = deployed_env(self.venture.env, config);
336 if env == self.venture.env {
337 return Arc::clone(&self.venture);
338 }
339 let mut deployed = (*self.venture).clone();
340 deployed.env = env;
341 Arc::new(deployed)
342 }
343
344 fn nest_modules(&self, ports: &Ports) -> Router {
350 let mut api = Router::new();
351 for module in &self.modules {
352 let ctx = self.module_context(module.as_ref(), ports);
353 api = api.nest(&format!("/v1/{}", module.name()), module.router(ctx));
354 }
355 api
356 }
357
358 fn production_readiness_now(
374 &self,
375 env: VentureEnv,
376 config: &dyn Config,
377 rate_limiter_ready: bool,
378 signer_ready: bool,
379 ) -> Vec<String> {
380 if let Some(note) = crate::route_policy::env_disagreement(self.venture.env, env) {
381 tracing::warn!("{note}");
382 crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, ¬e);
383 }
384 let unprotected = crate::route_policy::unprotected_writes_override(config);
388 let unlimited = crate::route_policy::unlimited_public_routes_override(config);
389 let guards = crate::route_policy::WriteGuards::collect(&self.modules);
390 let mut problems = crate::route_policy::production_readiness(
391 env,
392 &guards,
393 self.runtime.as_ref(),
394 rate_limiter_ready,
395 signer_ready,
396 unprotected.as_deref(),
397 unlimited.as_deref(),
398 );
399 let hmac_problems = crate::route_policy::webhook_secret_readiness(env, &guards, config);
404 if problems.is_empty() && hmac_problems.is_empty() {
405 let accepted = crate::route_policy::production_readiness(
414 env,
415 &guards,
416 self.runtime.as_ref(),
417 rate_limiter_ready,
418 signer_ready,
419 None,
420 None,
421 );
422 self.record_acceptances_once(
423 &guards,
424 rate_limiter_ready,
425 unprotected.as_deref(),
426 unlimited.as_deref(),
427 &accepted,
428 );
429 return problems;
430 }
431 for problem in &problems {
439 let detail = format!(
440 "refusing guarded routes: {problem} — set {} to a reason to accept this \
441 explicitly while the port is wired",
442 crate::route_policy::ALLOW_UNPROTECTED_WRITES
443 );
444 error!("{detail}");
445 crate::logging::forward_control_event(crate::logging::ControlLevel::Error, &detail);
446 }
447 problems.extend(hmac_problems);
448 problems
449 }
450
451 fn record_acceptances_once(
457 &self,
458 guards: &crate::route_policy::WriteGuards,
459 rate_limiter_ready: bool,
460 unprotected: Option<&str>,
461 unlimited: Option<&str>,
462 accepted: &[String],
463 ) {
464 if guards.needs_captcha()
465 && !crate::route_policy::captcha_effective(self.runtime.as_ref())
466 && let Some(reason) = unprotected
467 && !self
468 .unprotected_acceptance_recorded
469 .swap(true, Ordering::Relaxed)
470 {
471 let detail = format!(
472 "serving guarded routes unprotected on an operator's recorded acceptance \
473 (reason: {reason}; problems: {})",
474 accepted.join("; ")
475 );
476 tracing::warn!(
477 control = "production-readiness",
478 acceptance = crate::route_policy::ALLOW_UNPROTECTED_WRITES,
479 reason,
480 "serving captcha-guarded routes unprotected on an operator's recorded acceptance"
481 );
482 crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, &detail);
486 }
487 if guards.needs_rate_limiter()
488 && !rate_limiter_ready
489 && let Some(reason) = unlimited
490 && !self
491 .unlimited_acceptance_recorded
492 .swap(true, Ordering::Relaxed)
493 {
494 let detail = format!(
495 "serving public writes and admin routes without a rate limiter on an \
496 operator's recorded acceptance (reason: {reason}; problems: {})",
497 accepted.join("; ")
498 );
499 tracing::warn!(
500 control = "production-readiness",
501 acceptance = crate::route_policy::ALLOW_UNLIMITED_PUBLIC_ROUTES,
502 reason,
503 "serving public writes and admin routes without a rate limiter on an \
504 operator's recorded acceptance"
505 );
506 crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, &detail);
508 }
509 }
510
511 fn nest_sidecars(
514 mut api: Router,
515 mounted: &[SidecarMount],
516 ports: &Ports,
517 gateway: Option<&Arc<HmacSigner>>,
518 ) -> Router {
519 for mount in mounted {
520 api = api.nest(
521 &format!("/v1/{}", mount.name),
522 crate::sidecar::router(
523 mount.clone(),
524 ports.dispatcher.clone(),
525 Arc::clone(&ports.config),
526 ports.rate_limiter.clone(),
527 gateway.map(Arc::clone),
528 ),
529 );
530 }
531 api
532 }
533
534 fn sidecar_mounts(&self, ports: &Ports) -> Vec<SidecarMount> {
535 let mounts = match crate::sidecar::SidecarMounts::from_config(ports.config.as_ref()) {
536 Ok(mounts) => mounts,
537 Err(errors) => {
538 for error in errors {
539 let detail = format!("ignoring the sidecar mount table: {error}");
540 tracing::error!(error, "ignoring the sidecar mount table");
541 crate::logging::forward_control_event(
544 crate::logging::ControlLevel::Error,
545 &detail,
546 );
547 }
548 crate::sidecar::SidecarMounts::default()
549 }
550 };
551 let module_names: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
552 for collision in mounts.collisions(&module_names) {
553 let detail = format!("ignoring the colliding sidecar mount: {collision}");
554 tracing::error!(error = collision, "ignoring the colliding sidecar mount");
555 crate::logging::forward_control_event(crate::logging::ControlLevel::Error, &detail);
556 }
557 mounts
558 .iter()
559 .filter(|m| !module_names.contains(&m.name.as_str()))
560 .cloned()
561 .collect()
562 }
563
564 fn resolved_mounts(&self, ports: &Ports) -> Vec<SidecarMount> {
568 let mounts =
569 crate::sidecar::SidecarMounts::from_config(ports.config.as_ref()).unwrap_or_default();
570 let module_names: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
571 mounts
572 .iter()
573 .filter(|m| !module_names.contains(&m.name.as_str()))
574 .cloned()
575 .collect()
576 }
577
578 fn events_for(&self, ports: &Ports) -> EventBus {
586 let mounts = self.resolved_mounts(ports);
587 if mounts.is_empty() {
588 return self.events.clone();
589 }
590 let Some(dispatcher) = ports.dispatcher.clone() else {
591 return self.events.clone();
592 };
593 let gateway = gateway_signer(
594 ports.config.as_ref(),
595 ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
596 );
597 self.events
598 .clone()
599 .forwarding_to(crate::sidecar::EventForwarder::new(
600 mounts, dispatcher, gateway,
601 ))
602 }
603
604 fn ui_router(
609 &self,
610 api: &Router,
611 ports: &Ports,
612 surface: &Arc<dyn SurfaceSource>,
613 ) -> Option<Router> {
614 self.ui.as_ref().map(|ui| {
615 ui.router(UiContext {
616 surface: Arc::clone(surface),
617 api: api.clone(),
618 config: Arc::clone(&ports.config),
619 venture: Arc::clone(&self.venture),
620 captcha_configured: ports.captcha.is_some(),
621 signer: ports.signer.clone(),
622 rate_limiter: ports.rate_limiter.clone(),
623 })
624 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
625 })
626 }
627
628 fn inbound_events_route(&self, gateway: Option<Arc<HmacSigner>>) -> Option<Router> {
642 gateway.as_ref()?;
643 Some(
644 Router::new()
645 .route(
646 "/__events",
647 post(crate::sidecar::events_inbound)
648 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)),
649 )
650 .with_state(Arc::new(crate::sidecar::InboundEvents {
651 bus: self.events.clone(),
652 gateway,
653 })),
654 )
655 }
656
657 pub fn runtime(&self) -> Option<&Arc<dyn Runtime>> {
659 self.runtime.as_ref()
660 }
661
662 #[must_use]
666 pub fn surface(&self) -> &SurfaceDocument {
667 &self.surface.document
668 }
669
670 fn health_state(&self, ports: &Ports, sidecars: Vec<SidecarMount>) -> HealthState {
684 HealthState {
685 venture: self.venture_as_deployed(ports.config.as_ref()),
690 modules: self.modules.clone(),
691 harness_build: ports.config.get("HARNESS_BUILD").filter(|b| !b.is_empty()),
692 mailer_configured: ports.mailer.is_some(),
693 captcha_configured: ports.captcha.is_some(),
694 sidecars,
695 dispatcher: ports.dispatcher.clone(),
696 clock: ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
697 probe_cache: new_probe_cache(),
698 }
699 }
700
701 pub fn router(&self, ports: Ports) -> Router {
702 let env = deployed_env(self.venture.env, ports.config.as_ref());
707 let (limiter, signer) = (ports.rate_limiter.is_some(), ports.signer.is_some());
708 let readiness = self.production_readiness_now(env, ports.config.as_ref(), limiter, signer);
709 let mut api = self.nest_modules(&ports);
710 let gateway = gateway_signer(
713 ports.config.as_ref(),
714 ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
715 );
716 let mounted = self.sidecar_mounts(&ports);
717 api = Self::nest_sidecars(api, &mounted, &ports, gateway.as_ref());
718 let gateway_state = Self::gateway_guard_state(&ports, gateway.as_ref());
719 let mounts_for_health = mounted.clone();
720 let tenant_layer = Arc::new(TenantLayer {
722 routing: ports.tenants.clone(),
723 fallback: ports.db.clone(),
724 });
725 let api = api
741 .layer(from_fn_with_state(
742 ports.rate_limiter.clone(),
743 admin_rate_limit_layer,
744 ))
745 .layer(axum::middleware::from_fn_with_state(
746 tenant_layer,
747 resolve_tenant_layer,
748 ))
749 .layer(axum::middleware::from_fn(security_headers_layer))
750 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES));
751
752 let surface_source = self.merged_surface(mounted, &ports, gateway.clone(), env);
753
754 let ui = self.ui_router(&api, &ports, &surface_source);
755
756 let health_state = self.health_state(&ports, mounts_for_health);
757 let Ports {
758 config,
759 db,
760 auth: _,
761 mailer: _,
762 captcha: _,
763 rate_limiter: _,
764 signer: _,
765 kv: _,
766 blob: _,
767 push: _,
768 payments: _,
769 tracker: _,
770 realtime: _,
771 text_model: _,
772 classifier: _,
773 http: _,
774 clock,
775 id_gen,
776 defer,
777 dispatcher: _,
778 tenants: _,
779 } = ports;
780
781 let scope_state = ScopeState {
782 defer: defer.unwrap_or_else(|| Arc::new(crate::ports::NoopDefer)),
783 id_gen: id_gen.unwrap_or_else(|| Arc::new(crate::ports::UlidIdGen)),
784 module: (self.modules.len() == 1).then(|| self.modules[0].name()),
787 };
788 let ready_state = ReadyState {
789 db,
790 clock: clock.unwrap_or_else(|| Arc::new(SystemClock)),
791 };
792
793 let surface_state = SurfaceState {
794 config,
795 source: surface_source,
796 };
797
798 let root = Router::new()
799 .route("/__health", get(health_handler))
800 .with_state(health_state)
801 .route("/__ready", get(ready_handler))
802 .with_state(ready_state)
803 .route("/__surface", get(surface_handler))
804 .with_state(surface_state)
805 .merge(
808 self.inbound_events_route(gateway.clone())
809 .unwrap_or_default(),
810 )
811 .merge(api);
812 let root = match &self.well_known {
813 Some(well_known) => root.nest(
814 "/.well-known",
815 well_known
816 .clone()
817 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)),
818 ),
819 None => root,
820 };
821 let root = match ui {
822 Some(ui) => root.nest("/ui", ui),
823 None => root,
824 };
825
826 root.layer(from_fn_with_state(
835 Arc::new(readiness),
836 production_readiness_guard,
837 ))
838 .layer(from_fn_with_state(gateway_state, gateway_guard))
839 .layer(from_fn_with_state(scope_state, scope_layer))
840 .layer(axum::middleware::from_fn(token_response_layer))
841 .layer(cors_layer(&self.venture.cors_origins))
842 }
843}
844
845type ProbeCache = Option<(i64, Vec<serde_json::Value>)>;
848
849#[allow(clippy::disallowed_types)]
852fn new_probe_cache() -> Arc<std::sync::Mutex<ProbeCache>> {
853 Arc::new(std::sync::Mutex::new(None))
854}
855
856#[derive(Clone)]
857struct HealthState {
858 venture: Arc<Venture>,
859 modules: Vec<Arc<dyn Module>>,
860 harness_build: Option<String>,
862 mailer_configured: bool,
863 captcha_configured: bool,
864 sidecars: Vec<SidecarMount>,
865 dispatcher: Option<Arc<dyn Dispatcher>>,
866 clock: Arc<dyn Clock>,
867 #[allow(clippy::disallowed_types)]
875 probe_cache: Arc<std::sync::Mutex<ProbeCache>>,
876}
877
878const SIDECAR_PROBE_TTL_SECS: i64 = 30;
881
882const SIDECAR_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
885
886async fn health_handler(State(state): State<HealthState>) -> impl IntoResponse {
887 let modules: Vec<serde_json::Value> = state
888 .modules
889 .iter()
890 .map(|module| {
891 json!({
892 "name": module.name(),
893 "version": module.version(),
894 "requires": module.requires().iter().map(Port::name).collect::<Vec<_>>(),
895 "optional": module.optional().iter().map(Port::name).collect::<Vec<_>>(),
896 "tables": module.tables(),
897 "emits": module.emits(),
898 })
899 })
900 .collect();
901 let sidecars = probe_sidecars(&state).await;
902 Json(json!({
903 "venture": state.venture.name,
904 "env": state.venture.env.as_str(),
905 "harness_api": HARNESS_API,
906 "harness_build": state.harness_build,
907 "mailer": if state.mailer_configured { "configured" } else { "not_configured" },
910 "captcha": if state.captcha_configured { "configured" } else { "absent" },
911 "modules": modules,
912 "sidecars": sidecars,
913 }))
914}
915
916async fn probe_sidecars(state: &HealthState) -> Vec<serde_json::Value> {
922 if state.sidecars.is_empty() {
923 return Vec::new();
924 }
925 let now_ms =
926 i64::try_from(state.clock.now().unix_timestamp_nanos() / 1_000_000).unwrap_or(i64::MAX);
927 if let Some((at_ms, entries)) = state.probe_cache.lock().unwrap().as_ref()
928 && now_ms - at_ms < SIDECAR_PROBE_TTL_SECS * 1000
929 {
930 return entries.clone();
931 }
932
933 let mut entries = Vec::new();
934 for mount in &state.sidecars {
935 entries.push(probe_one_sidecar(state, mount).await);
936 }
937 *state.probe_cache.lock().unwrap() = Some((now_ms, entries.clone()));
938 entries
939}
940
941async fn probe_one_sidecar(state: &HealthState, mount: &SidecarMount) -> serde_json::Value {
942 let mut missing = json!({
943 "name": mount.name,
944 "binding": mount.binding,
945 "probe": "unreachable",
946 "contract": serde_json::Value::Null,
947 "module": serde_json::Value::Null,
948 "version": serde_json::Value::Null,
949 });
950 let Some(dispatcher) = state.dispatcher.as_ref() else {
951 return missing;
952 };
953 let request = http::Request::builder()
954 .method(http::Method::GET)
955 .uri("/__health")
956 .body(Bytes::new());
957 let Ok(request) = request else {
958 return missing;
959 };
960 let dispatcher = Arc::clone(dispatcher);
965 let binding = mount.binding.clone();
966 let probe = async move { dispatcher.dispatch(&binding, request).await };
967 let Some(Ok(response)) =
968 crate::ports::timeout(&*state.clock, probe, SIDECAR_PROBE_TIMEOUT).await
969 else {
970 return missing;
971 };
972 let contract = response
973 .headers()
974 .get(crate::sidecar::X_HARNESS_API)
975 .and_then(|v| v.to_str().ok())
976 .and_then(|v| v.parse::<u32>().ok());
977 let body = serde_json::from_slice::<serde_json::Value>(response.body()).ok();
978 let module = response
979 .headers()
980 .get(crate::sidecar::X_HARNESS_MODULE)
981 .and_then(|v| v.to_str().ok())
982 .map(str::to_owned)
983 .or_else(|| first_module_string(body.as_ref(), "name"));
984 let version = first_module_string(body.as_ref(), "version");
985 let tables = declared_tables(body.as_ref());
986 let contract = contract.or_else(|| {
987 body.as_ref()
988 .and_then(|b| b.get("harness_api"))
989 .and_then(serde_json::Value::as_u64)
990 .and_then(|api| u32::try_from(api).ok())
991 });
992 missing["contract"] = contract.map_or(serde_json::Value::Null, |api| json!(api));
993 missing["module"] = module.map_or(serde_json::Value::Null, |name| json!(name));
994 missing["version"] = version.map_or(serde_json::Value::Null, |version| json!(version));
995 missing["tables"] = json!(tables);
996 match contract {
997 Some(api) if api != HARNESS_API => missing["probe"] = json!("mismatch"),
998 Some(_) => missing["probe"] = json!("ok"),
999 None => missing["probe"] = json!("unreachable"),
1000 }
1001 let clashes = collisions_with(&tables, state, &mount.name);
1013 if !clashes.is_empty() {
1014 let detail = format!(
1015 "sidecar `{}` claims table(s) {} already claimed in this deployment",
1016 mount.name,
1017 clashes.join(", ")
1018 );
1019 tracing::error!(sidecar = %mount.name, tables = %clashes.join(","), "{detail}");
1020 crate::logging::forward_internal_error(&detail);
1021 missing["probe"] = json!("table-collision");
1022 missing["table_collisions"] = json!(clashes);
1023 }
1024 missing
1025}
1026
1027fn declared_tables(body: Option<&serde_json::Value>) -> Vec<String> {
1031 let Some(modules) = body
1032 .and_then(|b| b.get("modules"))
1033 .and_then(|m| m.as_array())
1034 else {
1035 return Vec::new();
1036 };
1037 let mut tables: Vec<String> = modules
1038 .iter()
1039 .filter_map(|module| module.get("tables")?.as_array())
1040 .flatten()
1041 .filter_map(|table| table.as_str().map(str::to_owned))
1042 .collect();
1043 tables.sort_unstable();
1044 tables.dedup();
1045 tables
1046}
1047
1048fn collisions_with(claimed: &[String], state: &HealthState, mount: &str) -> Vec<String> {
1051 let mut clashes: Vec<String> = Vec::new();
1052 for module in &state.modules {
1053 if module.name() == mount {
1054 continue;
1055 }
1056 for table in module.tables() {
1057 if claimed.iter().any(|claimed| claimed == table)
1058 && !clashes.iter().any(|seen| seen == table)
1059 {
1060 clashes.push(format!("`{table}` (also module `{}`)", module.name()));
1061 }
1062 }
1063 }
1064 clashes
1065}
1066
1067fn first_module_string(body: Option<&serde_json::Value>, key: &str) -> Option<String> {
1070 body?
1071 .get("modules")?
1072 .get(0)?
1073 .get(key)?
1074 .as_str()
1075 .map(str::to_owned)
1076}
1077
1078struct TenantLayer {
1081 routing: Option<Arc<dyn crate::tenant::TenantRouting>>,
1082 fallback: Option<Arc<dyn Database>>,
1088}
1089
1090async fn resolve_tenant_layer(
1097 State(layer): State<Arc<TenantLayer>>,
1098 scope: Scope,
1099 mut request: axum::extract::Request,
1100 next: Next,
1101) -> Response {
1102 let host = request
1103 .headers()
1104 .get(header::HOST)
1105 .and_then(|value| value.to_str().ok())
1106 .unwrap_or_default()
1107 .to_owned();
1108
1109 let resolution = match layer.routing.as_ref() {
1114 Some(routing) => routing.resolve(&host),
1115 None => <crate::tenant::ImplicitTenant as crate::tenant::ResolveTenant>::resolve(
1116 &crate::tenant::ImplicitTenant,
1117 &host,
1118 ),
1119 };
1120
1121 let tenant = match resolution.admit() {
1122 Ok(tenant) => tenant,
1123 Err(problem) => {
1124 return Problem::new(problem)
1125 .instance(&scope.request_id)
1126 .into_response();
1127 }
1128 };
1129
1130 let db = match layer.routing.as_ref() {
1131 Some(routing) => match routing.database(&tenant).await {
1132 Ok(db) => Some(db),
1133 Err(err) => {
1134 tracing::error!(tenant = %tenant.id(), "{err}");
1137 crate::logging::forward_internal_error(&err.to_string());
1138 return Problem::new(&SLUGS.tenant_degraded)
1139 .instance(&scope.request_id)
1140 .into_response();
1141 }
1142 },
1143 None => layer.fallback.clone(),
1144 };
1145
1146 if let Some(db) = db {
1147 let tenancy = if layer.routing.is_some() {
1152 crate::tenant::Tenancy::FromRegistry
1153 } else {
1154 crate::tenant::Tenancy::Sole
1155 };
1156 request
1157 .extensions_mut()
1158 .insert(crate::tenant_conn::ResolvedTenant {
1159 tenant,
1160 tenancy,
1161 db,
1162 });
1163 }
1164 next.run(request).await
1165}
1166
1167#[derive(Clone)]
1168struct ReadyState {
1169 db: Option<Arc<dyn Database>>,
1170 clock: Arc<dyn Clock>,
1171}
1172
1173async fn ready_handler(State(state): State<ReadyState>) -> impl IntoResponse {
1177 let Some(db) = state.db else {
1178 return Problem::not_ready("database port is not configured").into_response();
1179 };
1180 let stmt = Statement::new("SELECT 1");
1181 let query = async move { db.query(&stmt).await };
1182 match crate::ports::timeout(&*state.clock, query, Duration::from_secs(2)).await {
1183 Some(Ok(_rows)) => Json(json!({ "ok": true })).into_response(),
1184 Some(Err(err)) => {
1185 error!(error = %err, "readiness probe query failed");
1186 crate::logging::forward_internal_error(&format!("readiness probe query failed: {err}"));
1187 Problem::not_ready("database query failed").into_response()
1188 }
1189 None => Problem::not_ready("database did not answer within 2 s").into_response(),
1190 }
1191}
1192
1193#[derive(Clone)]
1194struct SurfaceState {
1195 config: Arc<dyn Config>,
1196 source: Arc<dyn SurfaceSource>,
1197}
1198
1199struct MergedSurface {
1206 base: Arc<SurfaceVariants>,
1207 mounts: Vec<SidecarMount>,
1208 dispatcher: Option<Arc<dyn Dispatcher>>,
1209 gateway: Option<Arc<HmacSigner>>,
1212 env: VentureEnv,
1213 captcha_configured: bool,
1214}
1215
1216impl MergedSurface {
1217 async fn sidecar_modules(&self) -> Vec<crate::surface::ModuleSurface> {
1219 let mut extra = Vec::new();
1220 let Some(dispatcher) = &self.dispatcher else {
1221 return extra;
1222 };
1223 for mount in &self.mounts {
1224 if !dispatcher.has(&mount.binding) {
1225 continue;
1226 }
1227 let mut builder = axum::http::Request::builder()
1228 .method(axum::http::Method::GET)
1229 .uri("/__surface")
1230 .header(header::ACCEPT, "application/json");
1231 if let Some(signer) = &self.gateway {
1232 builder =
1234 builder.header(X_HARNESS_GATEWAY, mint_gateway(signer, &mount.name, false));
1235 }
1236 let request = builder
1237 .body(bytes::Bytes::new())
1238 .expect("static request builds");
1239 let answer = match dispatcher.dispatch(&mount.binding, request).await {
1240 Ok(response) if response.status().is_success() => response,
1241 Ok(response) => {
1242 tracing::warn!(module = mount.name, status = %response.status(), "sidecar surface not available");
1243 continue;
1244 }
1245 Err(err) => {
1246 tracing::warn!(module = mount.name, error = %err, "sidecar surface fetch failed");
1247 continue;
1248 }
1249 };
1250 if answer.body().len() > MAX_SIDECAR_SURFACE_BYTES {
1253 tracing::warn!(
1254 module = mount.name,
1255 "sidecar surface exceeds {MAX_SIDECAR_SURFACE_BYTES} bytes and was not merged"
1256 );
1257 continue;
1258 }
1259 match serde_json::from_slice::<SurfaceDocument>(answer.body()) {
1260 Ok(document) => match sanitize_sidecar_document(&document, &mount.name) {
1261 Ok(entries) => extra.extend(entries.into_iter().filter_map(|mut entry| {
1262 if self.env == VentureEnv::Production
1266 && !self.captcha_configured
1267 && strip_unguarded_captcha_actions(&mut entry.surface) > 0
1268 {
1269 tracing::warn!(
1270 module = entry.name,
1271 "merged sidecar actions requiring a captcha were dropped: no Captcha port here"
1272 );
1273 }
1274 (!entry.surface.is_empty()).then_some(entry)
1275 })),
1276 Err(problems) => {
1277 for problem in problems {
1278 tracing::warn!(module = mount.name, error = problem, "sidecar surface rejected at merge");
1279 }
1280 }
1281 },
1282 Err(err) => {
1283 tracing::warn!(module = mount.name, error = %err, "sidecar surface is not a surface document");
1284 }
1285 }
1286 }
1287 extra
1288 }
1289}
1290
1291#[async_trait::async_trait]
1292impl SurfaceSource for MergedSurface {
1293 async fn current(&self) -> Arc<SurfaceDocument> {
1294 if self.mounts.is_empty() {
1295 return Arc::clone(&self.base.document);
1296 }
1297 let mut document = (*self.base.document).clone();
1298 document.modules.extend(self.sidecar_modules().await);
1299 Arc::new(document)
1300 }
1301
1302 fn built(&self) -> Arc<SurfaceDocument> {
1303 Arc::clone(&self.base.document)
1304 }
1305
1306 fn rendered(&self, admin: bool) -> Option<&RenderedSurface> {
1307 Some(if admin {
1308 &self.base.full
1309 } else {
1310 &self.base.public
1311 })
1312 }
1313}
1314
1315async fn surface_handler(
1331 State(state): State<SurfaceState>,
1332 headers: HeaderMap,
1333) -> impl IntoResponse {
1334 let admin = require_admin(&*state.config, &headers).is_ok();
1335 let current = state.source.current().await;
1339 let built = state.source.built();
1340 let prerendered = Arc::ptr_eq(¤t, &built).then(|| state.source.rendered(admin));
1341 let fresh;
1342 let rendered: &RenderedSurface = if let Some(rendered) = prerendered.flatten() {
1343 rendered
1344 } else {
1345 fresh = if admin {
1346 RenderedSurface::render(¤t)
1347 } else {
1348 RenderedSurface::render(¤t.public())
1349 };
1350 &fresh
1351 };
1352 let matches = headers
1353 .get(header::IF_NONE_MATCH)
1354 .and_then(|value| value.to_str().ok())
1355 .is_some_and(|value| {
1356 value
1357 .split(',')
1358 .map(str::trim)
1359 .any(|tag| tag == "*" || tag == rendered.etag)
1360 });
1361 let mut response = if matches {
1362 StatusCode::NOT_MODIFIED.into_response()
1363 } else {
1364 (
1365 [(header::CONTENT_TYPE, "application/json")],
1366 rendered.json.clone(),
1367 )
1368 .into_response()
1369 };
1370 let response_headers = response.headers_mut();
1371 response_headers.insert(
1372 header::ETAG,
1373 header::HeaderValue::from_str(&rendered.etag).expect("hex etag is a valid header"),
1374 );
1375 response_headers.insert(
1376 header::CACHE_CONTROL,
1377 if admin {
1378 header::HeaderValue::from_static("private, no-store")
1379 } else {
1380 header::HeaderValue::from_static("no-cache")
1381 },
1382 );
1383 response_headers.insert(
1384 header::VARY,
1385 header::HeaderValue::from_static("Authorization"),
1386 );
1387 response
1388}
1389
1390#[derive(Default)]
1393pub struct HarnessBuilder {
1394 venture: Option<Venture>,
1395 modules: Vec<Arc<dyn Module>>,
1396 provides: Vec<Port>,
1397 runtime: Option<Arc<dyn Runtime>>,
1398 module_templates: Vec<(String, Box<dyn Template>)>,
1399 overrides: Vec<(String, Box<dyn Template>)>,
1400 ui: Option<Arc<dyn UiMount>>,
1401}
1402
1403impl HarnessBuilder {
1404 #[must_use]
1405 pub fn venture(mut self, venture: Venture) -> Self {
1406 self.venture = Some(venture);
1407 self
1408 }
1409
1410 #[must_use]
1413 pub fn module(mut self, module: impl Module) -> Self {
1414 self.modules.push(Arc::new(module));
1415 self
1416 }
1417
1418 #[must_use]
1421 pub fn module_arc(mut self, module: Arc<dyn Module>) -> Self {
1422 self.modules.push(module);
1423 self
1424 }
1425
1426 #[must_use]
1430 pub fn runtime(mut self, runtime: impl Runtime) -> Self {
1431 let runtime: Arc<dyn Runtime> = Arc::new(runtime);
1432 self.provides = runtime.provides();
1433 self.runtime = Some(runtime);
1434 self
1435 }
1436
1437 #[must_use]
1440 pub fn templates(
1441 mut self,
1442 templates: impl IntoIterator<Item = (String, Box<dyn Template>)>,
1443 ) -> Self {
1444 self.module_templates.extend(templates);
1445 self
1446 }
1447
1448 #[must_use]
1451 pub fn ui(mut self, ui: impl UiMount) -> Self {
1452 self.ui = Some(Arc::new(ui));
1453 self
1454 }
1455
1456 #[must_use]
1459 pub fn template(mut self, id: impl Into<String>, template: Box<dyn Template>) -> Self {
1460 self.overrides.push((id.into(), template));
1461 self
1462 }
1463
1464 pub fn build(self) -> Result<Harness, ConfigError> {
1475 let mut errors = ConfigError::default();
1476
1477 let venture = if let Some(venture) = self.venture {
1478 venture.validate(&mut errors);
1479 venture
1480 } else {
1481 errors.push("missing venture: call .venture(Venture::new(..)) before .build()");
1482 Venture::new("invalid", "invalid.invalid")
1483 };
1484
1485 let mut names: HashMap<&'static str, usize> = HashMap::new();
1486 let mut tables: HashMap<&'static str, &'static str> = HashMap::new();
1487
1488 for module in &self.modules {
1489 if module.harness_api() != HARNESS_API {
1490 errors.push(harness_api_mismatch(module.as_ref()));
1491 }
1492
1493 let name = module.name();
1494 if name.is_empty() || !is_module_name(name) {
1495 errors.push(format!(
1496 "module name `{name}` must be kebab-case ([a-z0-9]+ separated by '-')"
1497 ));
1498 }
1499 if names.insert(name, 1).is_some() {
1500 errors.push(format!("duplicate module name `{name}`"));
1501 }
1502
1503 for port in module.requires().iter().chain(module.optional()) {
1504 if !Port::ALL.contains(port) {
1505 errors.push(format!(
1506 "module `{name}` declares unknown port {}",
1507 port.name()
1508 ));
1509 }
1510 }
1511 for port in module.requires() {
1512 if module.optional().contains(port) {
1513 errors.push(format!(
1514 "module `{name}` lists port {} in both requires() and optional()",
1515 port.name()
1516 ));
1517 }
1518 }
1519
1520 module.surface().validate(name, &mut errors);
1521
1522 check_tables(module.as_ref(), &mut tables, &mut errors);
1523 }
1524
1525 let well_known = collect_well_known(&self.modules, &mut errors);
1526
1527 for module in &self.modules {
1528 for port in module.requires() {
1529 if !self.provides.contains(port) {
1530 errors.push(format!(
1531 "module `{}` requires port {} which the runtime does not provide",
1532 module.name(),
1533 port.name()
1534 ));
1535 }
1536 }
1537 warn_undeclared_ports(module.as_ref(), &self.provides);
1538 }
1539
1540 let ordered = resolve_dependency_order(&self.modules, &mut errors);
1541
1542 append_production_readiness(&venture, &self.modules, self.runtime.as_ref(), &mut errors);
1543
1544 check_template_ids(
1545 self.overrides.iter().chain(self.module_templates.iter()),
1546 &names,
1547 &mut errors,
1548 );
1549
1550 let surface = Arc::new(SurfaceVariants::compose(
1551 &venture,
1552 &self.modules,
1553 self.ui.as_ref(),
1554 ));
1555 if let Some(ui) = &self.ui {
1556 ui.validate(&surface.document, &mut errors);
1557 }
1558
1559 errors.into_result()?;
1560
1561 let mut registry = TemplateRegistry::new();
1562 registry.register_all(self.module_templates);
1563 registry.register_all(self.overrides);
1564
1565 let mut events = EventBus::new();
1566 for module in &self.modules {
1567 for (name, handler) in module.events() {
1568 events = events.on(name, handler);
1569 }
1570 }
1571
1572 Ok(Harness {
1573 venture: Arc::new(venture),
1574 unprotected_acceptance_recorded: AtomicBool::new(false),
1575 unlimited_acceptance_recorded: AtomicBool::new(false),
1576 personal_data: Arc::new(crate::PersonalDataCatalog::compose(
1582 ordered.iter().map(|m| (m.name(), m.personal_data())),
1583 )),
1584 modules: ordered,
1585 templates: Arc::new(registry),
1586 events,
1587 runtime: self.runtime,
1588 well_known,
1589 surface,
1590 ui: self.ui,
1591 })
1592 }
1593}
1594
1595fn resolve_dependency_order(
1608 modules: &[Arc<dyn Module>],
1609 errors: &mut ConfigError,
1610) -> Vec<Arc<dyn Module>> {
1611 let names: BTreeSet<&str> = modules.iter().map(|module| module.name()).collect();
1612 let mut unknown = false;
1613 for module in modules {
1614 for needed in module.depends_on() {
1615 if !names.contains(needed) {
1616 unknown = true;
1617 errors.push(format!(
1618 "module `{}` depends on `{needed}`, which this venture does not compose: \
1619 add the module or drop the dependency",
1620 module.name()
1621 ));
1622 }
1623 }
1624 }
1625 if unknown {
1626 return modules.to_vec();
1627 }
1628
1629 let mut remaining: Vec<Option<Arc<dyn Module>>> = modules
1633 .iter()
1634 .map(|module| Some(Arc::clone(module)))
1635 .collect();
1636 let mut placed: BTreeSet<&str> = BTreeSet::new();
1637 let mut ordered: Vec<Arc<dyn Module>> = Vec::with_capacity(modules.len());
1638
1639 while ordered.len() < modules.len() {
1640 let mut progressed = false;
1641 for slot in &mut remaining {
1642 let ready = slot.as_ref().is_some_and(|module| {
1643 module
1644 .depends_on()
1645 .iter()
1646 .all(|needed| placed.contains(needed))
1647 });
1648 if ready {
1649 let module = slot.take().expect("checked above");
1650 placed.insert(module.name());
1651 ordered.push(module);
1652 progressed = true;
1653 }
1654 }
1655 if !progressed {
1656 let stuck: Vec<&str> = remaining
1658 .iter()
1659 .filter_map(|slot| slot.as_ref().map(|module| module.name()))
1660 .collect();
1661 errors.push(format!(
1662 "modules [{}] depend on each other in a cycle: migrations cannot be ordered so \
1663 that every module follows the ones it sits on top of",
1664 stuck.join(", ")
1665 ));
1666 return modules.to_vec();
1667 }
1668 }
1669 ordered
1670}
1671
1672fn check_template_ids<'a>(
1673 ids: impl Iterator<Item = &'a (String, Box<dyn Template>)>,
1674 names: &HashMap<&'static str, usize>,
1675 errors: &mut ConfigError,
1676) {
1677 for (id, _) in ids {
1678 let Some(module_name) = id.split('/').next() else {
1679 continue;
1680 };
1681 if !names.contains_key(module_name) {
1682 errors.push(format!(
1683 "template `{id}` names module `{module_name}` which is not registered"
1684 ));
1685 }
1686 }
1687}
1688
1689fn is_module_name(name: &str) -> bool {
1690 name.split('-').all(|part| {
1691 !part.is_empty()
1692 && part
1693 .chars()
1694 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
1695 })
1696}
1697
1698async fn production_readiness_guard(
1708 State(problems): State<Arc<Vec<String>>>,
1709 request: axum::extract::Request,
1710 next: Next,
1711) -> Response {
1712 if problems.is_empty() || !request.uri().path().starts_with("/v1/") {
1713 return next.run(request).await;
1714 }
1715 let problem = Problem::new(&SLUGS.not_production_ready).with_detail(problems.join("; "));
1716 match request.extensions().get::<Scope>() {
1717 Some(scope) => problem.instance(&scope.request_id.clone()),
1718 None => problem,
1719 }
1720 .into_response()
1721}
1722
1723async fn admin_rate_limit_layer(
1732 State(limiter): State<Option<Arc<dyn RateLimiter>>>,
1733 request: axum::extract::Request,
1734 next: Next,
1735) -> Response {
1736 if !crate::sidecar::is_admin_path(request.uri().path()) {
1737 return next.run(request).await;
1738 }
1739 let keys: Vec<String> = crate::rate_limit::rate_limit_keys(
1740 crate::rate_limit::client_ip(request.headers()).as_deref(),
1741 None,
1742 )
1743 .into_iter()
1744 .map(|key| format!("admin:{key}"))
1745 .collect();
1746 if let RateLimit::Denied { decision } =
1747 crate::rate_limit::check_rate_limit(limiter.as_ref(), &keys, RateLimitFailure::FailClosed)
1748 .await
1749 {
1750 return rate_limited(&decision);
1751 }
1752 next.run(request).await
1753}
1754
1755fn append_production_readiness(
1767 venture: &Venture,
1768 modules: &[Arc<dyn Module>],
1769 runtime: Option<&Arc<dyn Runtime>>,
1770 errors: &mut ConfigError,
1771) {
1772 for error in crate::route_policy::production_readiness(
1773 venture.env,
1774 &crate::route_policy::WriteGuards::collect(modules),
1775 runtime,
1776 crate::route_policy::rate_limiter_effective(runtime),
1782 crate::route_policy::signer_effective(runtime),
1783 None,
1784 None,
1785 ) {
1786 errors.push(error);
1787 }
1788}
1789
1790fn collect_well_known(modules: &[Arc<dyn Module>], errors: &mut ConfigError) -> Option<Router> {
1791 let mut providers: Vec<&'static str> = Vec::new();
1792 let mut well_known = None;
1793 for module in modules {
1794 if let Some(router) = module.well_known() {
1795 providers.push(module.name());
1796 well_known = Some(router);
1797 }
1798 }
1799 if providers.len() > 1 {
1800 let listed = providers
1801 .iter()
1802 .map(|name| format!("`{name}`"))
1803 .collect::<Vec<_>>()
1804 .join(", ");
1805 errors.push(format!(
1806 "modules {listed} all provide a well-known router; at most one module may occupy /.well-known"
1807 ));
1808 }
1809 well_known
1810}
1811
1812#[cfg(test)]
1813mod acceptance_recording {
1814 use super::*;
1820 use crate::Migrations;
1821 use crate::config::MapConfig;
1822
1823 struct PubWriter;
1826
1827 impl Module for PubWriter {
1828 fn name(&self) -> &'static str {
1829 "writer"
1830 }
1831 fn version(&self) -> &'static str {
1832 "0.0.0-test"
1833 }
1834 fn requires(&self) -> &'static [Port] {
1835 &[]
1836 }
1837 fn public_writes(&self) -> bool {
1838 true
1839 }
1840 fn migrations(&self) -> Migrations {
1841 Migrations::default()
1842 }
1843 fn validate_config(&self, _: &dyn Config) -> Result<(), ConfigError> {
1844 Ok(())
1845 }
1846 fn router(&self, _: ModuleContext) -> Router {
1847 Router::new()
1848 }
1849 }
1850
1851 struct AllPorts;
1854
1855 impl Runtime for AllPorts {
1856 fn provides(&self) -> Vec<Port> {
1857 Port::ALL.to_vec()
1858 }
1859 }
1860
1861 fn harness() -> Harness {
1862 Harness::builder()
1863 .venture(
1864 Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
1865 )
1866 .module(PubWriter)
1867 .runtime(AllPorts)
1868 .build()
1869 .expect("a development venture builds")
1870 }
1871
1872 fn deployed(unlimited: Option<&str>) -> MapConfig {
1873 let pairs = [("ENV", "production")].into_iter().chain(
1874 unlimited.map(|reason| (crate::route_policy::ALLOW_UNLIMITED_PUBLIC_ROUTES, reason)),
1875 );
1876 MapConfig::from_pairs(pairs)
1877 }
1878
1879 #[test]
1880 fn the_waiver_is_recorded_once_and_only_when_it_waived() {
1881 let harness = harness();
1882 let config = deployed(Some("issue #437: limiter binding pending"));
1883 assert!(
1884 harness
1885 .production_readiness_now(VentureEnv::Production, &config, false, true)
1886 .is_empty(),
1887 "the recorded acceptance serves"
1888 );
1889 assert!(
1890 harness
1891 .unlimited_acceptance_recorded
1892 .load(Ordering::Relaxed)
1893 );
1894
1895 assert!(
1898 harness
1899 .production_readiness_now(VentureEnv::Production, &config, false, true)
1900 .is_empty()
1901 );
1902 }
1903
1904 #[test]
1905 fn nothing_is_recorded_when_the_port_was_fine_all_along() {
1906 let harness = harness();
1907 let config = deployed(Some("unused: the limiter resolved"));
1908 assert!(
1909 harness
1910 .production_readiness_now(VentureEnv::Production, &config, true, true)
1911 .is_empty()
1912 );
1913 assert!(
1914 !harness
1915 .unlimited_acceptance_recorded
1916 .load(Ordering::Relaxed),
1917 "a waiver that removed no refusal is not a recorded acceptance"
1918 );
1919 }
1920
1921 #[test]
1922 fn a_refusing_deployment_records_no_acceptance_at_all() {
1923 let harness = harness();
1924 let config = MapConfig::from_pairs([
1928 ("ENV", "production"),
1929 (
1930 crate::route_policy::ALLOW_UNPROTECTED_WRITES,
1931 "captcha pending",
1932 ),
1933 ]);
1934 assert_eq!(
1935 harness
1936 .production_readiness_now(VentureEnv::Production, &config, false, true)
1937 .len(),
1938 1
1939 );
1940 assert!(
1941 !harness
1942 .unprotected_acceptance_recorded
1943 .load(Ordering::Relaxed)
1944 );
1945 assert!(
1946 !harness
1947 .unlimited_acceptance_recorded
1948 .load(Ordering::Relaxed)
1949 );
1950 }
1951
1952 struct HmacWriter;
1956
1957 impl Module for HmacWriter {
1958 fn name(&self) -> &'static str {
1959 "webhooks"
1960 }
1961 fn version(&self) -> &'static str {
1962 "0.0.0-test"
1963 }
1964 fn requires(&self) -> &'static [Port] {
1965 &[]
1966 }
1967 fn public_writes(&self) -> bool {
1968 true
1969 }
1970 fn public_write_policy(&self) -> crate::route_policy::RoutePolicy {
1971 crate::route_policy::RoutePolicy::Signature
1972 }
1973 fn signature_verification(&self) -> crate::route_policy::SignatureVerification {
1974 crate::route_policy::SignatureVerification::Hmac {
1975 secret: "WEBHOOK_SECRET",
1976 }
1977 }
1978 fn migrations(&self) -> Migrations {
1979 Migrations::default()
1980 }
1981 fn validate_config(&self, _: &dyn Config) -> Result<(), ConfigError> {
1982 Ok(())
1983 }
1984 fn router(&self, _: ModuleContext) -> Router {
1985 Router::new()
1986 }
1987 }
1988
1989 #[test]
1990 fn a_non_payments_signature_venture_boots_on_its_webhook_secret_alone() {
1991 let harness = Harness::builder()
1992 .venture(
1993 Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
1994 )
1995 .module(HmacWriter)
1996 .runtime(AllPorts)
1997 .build()
1998 .expect("a development venture builds");
1999 let deployed = MapConfig::from_pairs([("ENV", "production")]);
2000 let problems =
2001 harness.production_readiness_now(VentureEnv::Production, &deployed, true, true);
2002 assert_eq!(problems.len(), 1, "{problems:?}");
2003 assert!(
2004 problems[0].contains("WEBHOOKS_WEBHOOK_SECRET"),
2005 "{}",
2006 problems[0]
2007 );
2008 assert!(
2009 !problems[0].contains("Payments"),
2010 "a module verifying through the HMAC scheme is not the Payments leg's business: {}",
2011 problems[0]
2012 );
2013
2014 let ready = MapConfig::from_pairs([
2016 ("ENV", "production"),
2017 ("WEBHOOKS_WEBHOOK_SECRET", "whsec-test-dummy"),
2018 ]);
2019 assert!(
2020 harness
2021 .production_readiness_now(VentureEnv::Production, &ready, true, true)
2022 .is_empty()
2023 );
2024 }
2025}