use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use axum::Router;
use axum::extract::{DefaultBodyLimit, State};
use axum::http::{HeaderMap, Method, StatusCode, header};
use axum::middleware::{Next, from_fn_with_state};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use bytes::Bytes;
use serde_json::json;
use tracing::error;
use crate::admin::require_admin;
use crate::config::Config;
use crate::config::ConfigError;
use crate::events::EventBus;
use crate::http::{
Json, MAX_BODY_BYTES, ScopeState, cors_layer, rate_limited, scope_layer,
security_headers_layer, token_response_layer,
};
use crate::module::{HARNESS_API, Module, ModuleContext, harness_api_mismatch};
use crate::ports::Dispatcher;
use crate::ports::{
ActorHandler, ActorHandlers, Clock, Database, Port, Ports, RateLimiter, Statement, SystemClock,
validate_actor_kind, warn_undeclared_ports,
};
use crate::problem::Problem;
use crate::problems::SLUGS;
use crate::rate_limit::{RateLimit, RateLimitFailure};
use crate::route_policy::deployed_env;
use crate::scheduled::ScheduledBudget;
use crate::scope::Scope;
use crate::sidecar::{
GatewayGuard, SIDECAR_REQUIRE_GATEWAY, SidecarMount, X_HARNESS_GATEWAY, gateway_guard,
gateway_signer, mint_gateway, truthy,
};
use crate::signer::HmacSigner;
use crate::stream::{RequestStream, StreamRoute};
use crate::surface::{
MAX_SIDECAR_SURFACE_BYTES, RenderedSurface, SurfaceDocument, SurfaceSource, UiContext, UiMount,
sanitize_sidecar_document, strip_unguarded_captcha_actions,
};
use crate::template::{Template, TemplateRegistry};
use crate::venture::{Venture, VentureEnv};
fn check_tables(
module: &dyn Module,
tables: &mut HashMap<&'static str, &'static str>,
errors: &mut ConfigError,
) {
let name = module.name();
for table in module.tables() {
match tables.get(table) {
Some(owner) => errors.push(format!(
"duplicate table `{table}` claimed by modules `{owner}` and `{name}`"
)),
None => {
tables.insert(table, name);
}
}
}
let owns = module.tables();
let mut declared: Vec<&'static str> = Vec::new();
for set in module.personal_data() {
for problem in set.validate(name, owns) {
errors.push(problem);
}
if declared.contains(&set.table) {
errors.push(format!(
"module `{name}` declares table `{}` twice in personal_data()",
set.table
));
}
declared.push(set.table);
}
}
fn collect_actors(
handlers: &[(String, Arc<dyn ActorHandler>)],
modules: &[Arc<dyn Module>],
errors: &mut ConfigError,
) -> ActorHandlers {
let mut registered: HashSet<&str> = HashSet::new();
let mut registry = ActorHandlers::new();
for (kind, handler) in handlers {
if let Err(err) = validate_actor_kind(kind) {
errors.push(format!(
"actor handler registered for invalid kind `{kind}`: {err}"
));
}
if !registered.insert(kind.as_str()) {
errors.push(format!(
"duplicate actor handler registered for kind `{kind}`"
));
}
registry = registry.with(kind, Arc::clone(handler));
}
let mut declared: HashMap<&'static str, &'static str> = HashMap::new();
for module in modules {
let name = module.name();
let kinds = module.actor_kinds();
if !kinds.is_empty()
&& !module.requires().contains(&Port::Actor)
&& !module.optional().contains(&Port::Actor)
{
errors.push(format!(
"module `{name}` declares actor kinds but lists port {} in neither requires() nor optional()",
Port::Actor.name(),
));
}
for kind in kinds {
if let Err(err) = validate_actor_kind(kind) {
errors.push(format!(
"module `{name}` declares invalid actor kind `{kind}`: {err}"
));
}
match declared.get(kind) {
Some(owner) => errors.push(format!(
"duplicate actor kind `{kind}` declared by modules `{owner}` and `{name}`"
)),
None => {
declared.insert(kind, name);
}
}
if !registered.contains(kind) {
errors.push(format!(
"module `{name}` declares actor kind `{kind}` with no registered handler"
));
}
}
}
registry
}
pub trait Runtime: Send + Sync + 'static {
fn provides(&self) -> Vec<Port>;
fn effectively_configured(&self, port: Port) -> bool {
self.provides().contains(&port)
}
}
pub struct Harness {
venture: Arc<Venture>,
modules: Vec<Arc<dyn Module>>,
templates: Arc<TemplateRegistry>,
events: EventBus,
runtime: Option<Arc<dyn Runtime>>,
well_known: Option<Router>,
surface: Arc<SurfaceVariants>,
actor_handlers: ActorHandlers,
ui: Option<Arc<dyn UiMount>>,
personal_data: Arc<crate::PersonalDataCatalog>,
unprotected_acceptance_recorded: AtomicBool,
unlimited_acceptance_recorded: AtomicBool,
}
struct SurfaceVariants {
document: Arc<SurfaceDocument>,
full: RenderedSurface,
public: RenderedSurface,
}
impl SurfaceVariants {
fn compose(
venture: &Venture,
modules: &[Arc<dyn Module>],
ui: Option<&Arc<dyn UiMount>>,
) -> Self {
let mut document = SurfaceDocument::compose(venture, modules);
document.ui = ui.and_then(|ui| ui.describe());
Self {
full: RenderedSurface::render(&document),
public: RenderedSurface::render(&document.public()),
document: Arc::new(document),
}
}
}
impl std::fmt::Debug for Harness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let modules: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
f.debug_struct("Harness")
.field("venture", &self.venture.name)
.field("modules", &modules)
.finish_non_exhaustive()
}
}
impl Harness {
#[must_use]
pub fn clone_arc(self: &std::sync::Arc<Self>) -> std::sync::Arc<Self> {
std::sync::Arc::clone(self)
}
pub fn builder() -> HarnessBuilder {
HarnessBuilder::default()
}
pub fn venture(&self) -> &Arc<Venture> {
&self.venture
}
pub fn modules(&self) -> &[Arc<dyn Module>] {
&self.modules
}
pub fn actor_handlers(&self) -> &ActorHandlers {
&self.actor_handlers
}
pub fn max_body_bytes(&self, path: &str, cfg: &dyn Config) -> usize {
let Some((name, _)) = module_and_segments(path) else {
return MAX_BODY_BYTES;
};
self.modules
.iter()
.find(|module| module.name() == name)
.map_or(MAX_BODY_BYTES, |module| {
MAX_BODY_BYTES.max(module.max_body_bytes(cfg))
})
}
pub fn streaming_route(&self, path: &str, method: &axum::http::Method) -> Option<usize> {
let (name, segments) = exact_module_and_segments(path)?;
let module = self.modules.iter().find(|module| module.name() == name)?;
match_streaming(module.streaming_routes(), method, &segments)
}
pub fn templates(&self) -> &Arc<TemplateRegistry> {
&self.templates
}
pub fn events(&self) -> &EventBus {
&self.events
}
pub fn module_context(&self, module: &dyn Module, ports: &Ports) -> ModuleContext {
let venture = self.venture_as_deployed(ports.config.as_ref());
ModuleContext {
config: Arc::clone(&ports.config),
ports: ports.view_for(module),
events: self.events_for(ports),
templates: Arc::clone(&self.templates),
venture,
unprotected_writes_accepted: crate::route_policy::unprotected_writes_override(
ports.config.as_ref(),
)
.is_some(),
personal_data: Arc::clone(&self.personal_data),
ui_mounted: self.ui.is_some(),
scheduled: Arc::new(ScheduledBudget::unbounded()),
}
}
fn gateway_guard_state(ports: &Ports, gateway: Option<&Arc<HmacSigner>>) -> GatewayGuard {
GatewayGuard {
require: truthy(ports.config.as_ref(), SIDECAR_REQUIRE_GATEWAY),
signer: gateway.map(Arc::clone),
admin_token: ports
.config
.get("ADMIN_TOKEN")
.filter(|token| !token.is_empty()),
}
}
fn merged_surface(
&self,
mounts: Vec<SidecarMount>,
ports: &Ports,
gateway: Option<Arc<HmacSigner>>,
env: VentureEnv,
) -> Arc<dyn SurfaceSource> {
Arc::new(MergedSurface {
base: Arc::clone(&self.surface),
mounts,
dispatcher: ports.dispatcher.clone(),
gateway,
env,
captcha_configured: ports.captcha.is_some(),
})
}
fn venture_as_deployed(&self, config: &dyn Config) -> Arc<Venture> {
let env = deployed_env(self.venture.env, config);
if env == self.venture.env {
return Arc::clone(&self.venture);
}
let mut deployed = (*self.venture).clone();
deployed.env = env;
Arc::new(deployed)
}
fn nest_modules(&self, ports: &Ports) -> Router {
let mut api = Router::new();
for module in &self.modules {
let ctx = self.module_context(module.as_ref(), ports);
api = api.nest(&format!("/v1/{}", module.name()), module.router(ctx));
}
api
}
fn production_readiness_now(
&self,
env: VentureEnv,
config: &dyn Config,
rate_limiter_ready: bool,
signer_ready: bool,
) -> Vec<String> {
if let Some(note) = crate::route_policy::env_disagreement(self.venture.env, env) {
tracing::warn!("{note}");
crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, ¬e);
}
let unprotected = crate::route_policy::unprotected_writes_override(config);
let unlimited = crate::route_policy::unlimited_public_routes_override(config);
let guards = crate::route_policy::WriteGuards::collect(&self.modules);
let mut problems = crate::route_policy::production_readiness(
env,
&guards,
self.runtime.as_ref(),
rate_limiter_ready,
signer_ready,
unprotected.as_deref(),
unlimited.as_deref(),
);
let hmac_problems = crate::route_policy::webhook_secret_readiness(env, &guards, config);
if problems.is_empty() && hmac_problems.is_empty() {
let accepted = crate::route_policy::production_readiness(
env,
&guards,
self.runtime.as_ref(),
rate_limiter_ready,
signer_ready,
None,
None,
);
self.record_acceptances_once(
&guards,
rate_limiter_ready,
unprotected.as_deref(),
unlimited.as_deref(),
&accepted,
);
return problems;
}
for problem in &problems {
let detail = format!(
"refusing guarded routes: {problem} — set {} to a reason to accept this \
explicitly while the port is wired",
crate::route_policy::ALLOW_UNPROTECTED_WRITES
);
error!("{detail}");
crate::logging::forward_control_event(crate::logging::ControlLevel::Error, &detail);
}
problems.extend(hmac_problems);
problems
}
fn record_acceptances_once(
&self,
guards: &crate::route_policy::WriteGuards,
rate_limiter_ready: bool,
unprotected: Option<&str>,
unlimited: Option<&str>,
accepted: &[String],
) {
if guards.needs_captcha()
&& !crate::route_policy::captcha_effective(self.runtime.as_ref())
&& let Some(reason) = unprotected
&& !self
.unprotected_acceptance_recorded
.swap(true, Ordering::Relaxed)
{
let detail = format!(
"serving guarded routes unprotected on an operator's recorded acceptance \
(reason: {reason}; problems: {})",
accepted.join("; ")
);
tracing::warn!(
control = "production-readiness",
acceptance = crate::route_policy::ALLOW_UNPROTECTED_WRITES,
reason,
"serving captcha-guarded routes unprotected on an operator's recorded acceptance"
);
crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, &detail);
}
if guards.needs_rate_limiter()
&& !rate_limiter_ready
&& let Some(reason) = unlimited
&& !self
.unlimited_acceptance_recorded
.swap(true, Ordering::Relaxed)
{
let detail = format!(
"serving public writes and admin routes without a rate limiter on an \
operator's recorded acceptance (reason: {reason}; problems: {})",
accepted.join("; ")
);
tracing::warn!(
control = "production-readiness",
acceptance = crate::route_policy::ALLOW_UNLIMITED_PUBLIC_ROUTES,
reason,
"serving public writes and admin routes without a rate limiter on an \
operator's recorded acceptance"
);
crate::logging::forward_control_event(crate::logging::ControlLevel::Warn, &detail);
}
}
fn nest_sidecars(
mut api: Router,
mounted: &[SidecarMount],
ports: &Ports,
gateway: Option<&Arc<HmacSigner>>,
) -> Router {
for mount in mounted {
api = api.nest(
&format!("/v1/{}", mount.name),
crate::sidecar::router(
mount.clone(),
ports.dispatcher.clone(),
Arc::clone(&ports.config),
ports.rate_limiter.clone(),
gateway.map(Arc::clone),
),
);
}
api
}
fn sidecar_mounts(&self, ports: &Ports) -> Vec<SidecarMount> {
let mounts = match crate::sidecar::SidecarMounts::from_config(ports.config.as_ref()) {
Ok(mounts) => mounts,
Err(errors) => {
for error in errors {
let detail = format!("ignoring the sidecar mount table: {error}");
tracing::error!(error, "ignoring the sidecar mount table");
crate::logging::forward_control_event(
crate::logging::ControlLevel::Error,
&detail,
);
}
crate::sidecar::SidecarMounts::default()
}
};
let module_names: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
for collision in mounts.collisions(&module_names) {
let detail = format!("ignoring the colliding sidecar mount: {collision}");
tracing::error!(error = collision, "ignoring the colliding sidecar mount");
crate::logging::forward_control_event(crate::logging::ControlLevel::Error, &detail);
}
mounts
.iter()
.filter(|m| !module_names.contains(&m.name.as_str()))
.cloned()
.collect()
}
fn resolved_mounts(&self, ports: &Ports) -> Vec<SidecarMount> {
let mounts =
crate::sidecar::SidecarMounts::from_config(ports.config.as_ref()).unwrap_or_default();
let module_names: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
mounts
.iter()
.filter(|m| !module_names.contains(&m.name.as_str()))
.cloned()
.collect()
}
fn events_for(&self, ports: &Ports) -> EventBus {
let mounts = self.resolved_mounts(ports);
if mounts.is_empty() {
return self.events.clone();
}
let Some(dispatcher) = ports.dispatcher.clone() else {
return self.events.clone();
};
let gateway = gateway_signer(
ports.config.as_ref(),
ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
);
self.events
.clone()
.forwarding_to(crate::sidecar::EventForwarder::new(
mounts, dispatcher, gateway,
))
}
fn ui_router(
&self,
api: &Router,
ports: &Ports,
surface: &Arc<dyn SurfaceSource>,
) -> Option<Router> {
self.ui.as_ref().map(|ui| {
ui.router(UiContext {
surface: Arc::clone(surface),
api: api.clone(),
config: Arc::clone(&ports.config),
venture: Arc::clone(&self.venture),
captcha_configured: ports.captcha.is_some(),
signer: ports.signer.clone(),
rate_limiter: ports.rate_limiter.clone(),
})
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
})
}
fn root_router(
&self,
api: Router,
ui: Option<Router>,
gateway: Option<Arc<HmacSigner>>,
health_state: HealthState,
ready_state: ReadyState,
surface_state: SurfaceState,
) -> Router {
let root = Router::new()
.route("/__health", get(health_handler))
.with_state(health_state)
.route("/__ready", get(ready_handler))
.with_state(ready_state)
.route("/__surface", get(surface_handler))
.with_state(surface_state)
.merge(self.inbound_events_route(gateway).unwrap_or_default())
.merge(api);
let root = match &self.well_known {
Some(well_known) => root.nest(
"/.well-known",
well_known
.clone()
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES)),
),
None => root,
};
match ui {
Some(ui) => root.nest("/ui", ui),
None => root,
}
}
fn inbound_events_route(&self, gateway: Option<Arc<HmacSigner>>) -> Option<Router> {
gateway.as_ref()?;
Some(
Router::new()
.route(
"/__events",
post(crate::sidecar::events_inbound)
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES)),
)
.with_state(Arc::new(crate::sidecar::InboundEvents {
bus: self.events.clone(),
gateway,
})),
)
}
pub fn runtime(&self) -> Option<&Arc<dyn Runtime>> {
self.runtime.as_ref()
}
#[must_use]
pub fn surface(&self) -> &SurfaceDocument {
&self.surface.document
}
fn health_state(&self, ports: &Ports, sidecars: Vec<SidecarMount>) -> HealthState {
HealthState {
venture: self.venture_as_deployed(ports.config.as_ref()),
modules: self.modules.clone(),
harness_build: ports.config.get("HARNESS_BUILD").filter(|b| !b.is_empty()),
mailer_configured: ports.mailer.is_some(),
captcha_configured: ports.captcha.is_some(),
sidecars,
dispatcher: ports.dispatcher.clone(),
clock: ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
probe_cache: new_probe_cache(),
}
}
pub fn router(&self, ports: Ports) -> Router {
let env = deployed_env(self.venture.env, ports.config.as_ref());
let (limiter, signer) = (ports.rate_limiter.is_some(), ports.signer.is_some());
let readiness = self.production_readiness_now(env, ports.config.as_ref(), limiter, signer);
let problem_type_base: Arc<str> = self.venture.problem_type_base().into();
let mut api = self.nest_modules(&ports);
let gateway = gateway_signer(
ports.config.as_ref(),
ports.clock.clone().unwrap_or_else(|| Arc::new(SystemClock)),
);
let mounted = self.sidecar_mounts(&ports);
api = Self::nest_sidecars(api, &mounted, &ports, gateway.as_ref());
let gateway_state = Self::gateway_guard_state(&ports, gateway.as_ref());
let mounts_for_health = mounted.clone();
let tenant_layer = Arc::new(TenantLayer {
routing: ports.tenants.clone(),
fallback: ports.db.clone(),
});
let streaming = StreamingState::build(&self.modules);
let api = api.layer(from_fn_with_state(streaming, streaming_request_layer));
let api = with_abuse_floor(api, ports.rate_limiter.clone(), tenant_layer);
let surface_source = self.merged_surface(mounted, &ports, gateway.clone(), env);
let ui = self.ui_router(&api, &ports, &surface_source);
let health_state = self.health_state(&ports, mounts_for_health);
let missing_ports = missing_module_ports(&self.modules, &ports);
let Ports {
config,
db,
auth: _,
mailer: _,
captcha: _,
rate_limiter: _,
signer: _,
kv: _,
blob: _,
push: _,
payments: _,
tracker: _,
realtime: _,
text_model: _,
classifier: _,
vector_index: _,
embedder: _,
custom_hostnames: _,
http: _,
clock,
id_gen,
defer,
actors: _,
dispatcher: _,
tenants: _,
} = ports;
let scope_state = ScopeState {
defer: defer.unwrap_or_else(|| Arc::new(crate::ports::NoopDefer)),
id_gen: id_gen.unwrap_or_else(|| Arc::new(crate::ports::UlidIdGen)),
module: (self.modules.len() == 1).then(|| self.modules[0].name()),
};
let ready_state = ReadyState {
db,
clock: clock.unwrap_or_else(|| Arc::new(SystemClock)),
missing_ports,
};
let surface_state = SurfaceState {
config,
source: surface_source,
};
let root = self.root_router(api, ui, gateway, health_state, ready_state, surface_state);
root.layer(from_fn_with_state(
Arc::new(readiness),
production_readiness_guard,
))
.layer(from_fn_with_state(gateway_state, gateway_guard))
.layer(from_fn_with_state(scope_state, scope_layer))
.layer(axum::middleware::from_fn(token_response_layer))
.layer(cors_layer(&self.venture.cors_origins))
.layer(from_fn_with_state(problem_type_base, problem_type_layer))
}
}
fn module_and_segments(path: &str) -> Option<(&str, Vec<&str>)> {
let path = match path.split_once(['?', '#']) {
Some((path, _)) => path,
None => path,
};
let mut segments = path.split('/').filter(|segment| !segment.is_empty());
if segments.next() != Some("v1") {
return None;
}
let name = segments.next()?;
Some((name, segments.collect()))
}
fn exact_module_and_segments(path: &str) -> Option<(&str, Vec<&str>)> {
let path = match path.split_once(['?', '#']) {
Some((path, _)) => path,
None => path,
};
let mut segments = path.split('/');
if segments.next() != Some("") || segments.next() != Some("v1") {
return None;
}
let name = segments.next()?;
if name.is_empty() {
return None;
}
Some((name, segments.collect()))
}
fn match_streaming(routes: &[StreamRoute], method: &Method, segments: &[&str]) -> Option<usize> {
routes.iter().find_map(|route| {
(route.method == *method && pattern_matches(route.path, segments))
.then_some(route.max_bytes)
})
}
fn pattern_matches(pattern: &str, segments: &[&str]) -> bool {
let mut pattern_segments = pattern.split('/').filter(|segment| !segment.is_empty());
let mut rest = segments.iter();
loop {
match pattern_segments.next() {
Some("{*rest}") => {
return pattern_segments.next().is_none() && rest.next().is_some();
}
Some(param) if param.starts_with('{') && param.ends_with('}') => {
if rest.next().is_none() {
return false;
}
}
Some(literal) => {
if rest.next() != Some(&literal) {
return false;
}
}
None => return rest.next().is_none(),
}
}
}
#[derive(Clone)]
struct StreamingState {
modules: Arc<Vec<(&'static str, &'static [StreamRoute])>>,
}
impl StreamingState {
fn build(modules: &[Arc<dyn Module>]) -> Self {
let routes = modules
.iter()
.filter(|module| !module.streaming_routes().is_empty())
.map(|module| (module.name(), module.streaming_routes()))
.collect();
Self {
modules: Arc::new(routes),
}
}
fn max_bytes(&self, path: &str, method: &Method) -> Option<usize> {
let (name, segments) = exact_module_and_segments(path)?;
let (_, routes) = self.modules.iter().find(|(module, _)| *module == name)?;
match_streaming(routes, method, &segments)
}
}
async fn streaming_request_layer(
State(state): State<StreamingState>,
mut request: axum::extract::Request,
next: Next,
) -> Response {
if state.modules.is_empty() {
return next.run(request).await;
}
let Some(max_bytes) = state.max_bytes(request.uri().path(), request.method()) else {
return next.run(request).await;
};
if request.extensions().get::<RequestStream>().is_some() {
return next.run(request).await;
}
if let Some(declared) = crate::ports::declared_content_length(request.headers())
&& declared > max_bytes
{
let problem = Problem::request_too_large();
let problem = match request.extensions().get::<Scope>() {
Some(scope) => problem.instance(&scope.request_id),
None => problem,
};
return problem.into_response();
}
let body = std::mem::replace(request.body_mut(), axum::body::Body::empty());
request
.extensions_mut()
.insert(RequestStream::new(body.into_data_stream(), max_bytes));
next.run(request).await
}
fn with_abuse_floor(
api: Router,
rate_limiter: Option<Arc<dyn RateLimiter>>,
tenant_layer: Arc<TenantLayer>,
) -> Router {
api.layer(from_fn_with_state(rate_limiter, admin_rate_limit_layer))
.layer(axum::middleware::from_fn_with_state(
tenant_layer,
resolve_tenant_layer,
))
.layer(axum::middleware::from_fn(security_headers_layer))
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
}
async fn problem_type_layer(
State(base): State<Arc<str>>,
request: axum::extract::Request,
next: Next,
) -> Response {
let mut response = next.run(request).await;
let Some(problem) = response.extensions().get::<Problem>().cloned() else {
return response;
};
let body = problem.body(&base).to_string();
*response.body_mut() = axum::body::Body::from(body);
response.headers_mut().remove(header::CONTENT_LENGTH);
response.extensions_mut().remove::<Problem>();
response
}
type ProbeCache = Option<(i64, Vec<serde_json::Value>)>;
fn new_probe_cache() -> Arc<futures_util::lock::Mutex<ProbeCache>> {
Arc::new(futures_util::lock::Mutex::new(None))
}
#[derive(Clone)]
struct HealthState {
venture: Arc<Venture>,
modules: Vec<Arc<dyn Module>>,
harness_build: Option<String>,
mailer_configured: bool,
captcha_configured: bool,
sidecars: Vec<SidecarMount>,
dispatcher: Option<Arc<dyn Dispatcher>>,
clock: Arc<dyn Clock>,
probe_cache: Arc<futures_util::lock::Mutex<ProbeCache>>,
}
const SIDECAR_PROBE_TTL_SECS: i64 = 30;
const SIDECAR_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
async fn health_handler(State(state): State<HealthState>) -> impl IntoResponse {
let modules: Vec<serde_json::Value> = state
.modules
.iter()
.map(|module| {
json!({
"name": module.name(),
"version": module.version(),
"requires": module.requires().iter().map(Port::name).collect::<Vec<_>>(),
"optional": module.optional().iter().map(Port::name).collect::<Vec<_>>(),
"tables": module.tables(),
"emits": module.emits(),
})
})
.collect();
let sidecars = probe_sidecars(&state).await;
Json(json!({
"venture": state.venture.name,
"env": state.venture.env.as_str(),
"harness_api": HARNESS_API,
"harness_build": state.harness_build,
"mailer": if state.mailer_configured { "configured" } else { "not_configured" },
"captcha": if state.captcha_configured { "configured" } else { "absent" },
"modules": modules,
"sidecars": sidecars,
}))
}
async fn probe_sidecars(state: &HealthState) -> Vec<serde_json::Value> {
if state.sidecars.is_empty() {
return Vec::new();
}
let mut cache = state.probe_cache.lock().await;
let now_ms =
i64::try_from(state.clock.now().unix_timestamp_nanos() / 1_000_000).unwrap_or(i64::MAX);
if let Some((at_ms, entries)) = cache.as_ref()
&& now_ms - at_ms < SIDECAR_PROBE_TTL_SECS * 1000
{
return entries.clone();
}
let mut entries = Vec::new();
for mount in &state.sidecars {
entries.push(probe_one_sidecar(state, mount).await);
}
let stamped_ms =
i64::try_from(state.clock.now().unix_timestamp_nanos() / 1_000_000).unwrap_or(i64::MAX);
*cache = Some((stamped_ms, entries.clone()));
entries
}
async fn probe_one_sidecar(state: &HealthState, mount: &SidecarMount) -> serde_json::Value {
let mut missing = json!({
"name": mount.name,
"binding": mount.binding,
"probe": "unreachable",
"contract": serde_json::Value::Null,
"module": serde_json::Value::Null,
"version": serde_json::Value::Null,
});
let Some(dispatcher) = state.dispatcher.as_ref() else {
return missing;
};
let request = http::Request::builder()
.method(http::Method::GET)
.uri("/__health")
.body(Bytes::new());
let Ok(request) = request else {
return missing;
};
let dispatcher = Arc::clone(dispatcher);
let binding = mount.binding.clone();
let probe = async move { dispatcher.dispatch(&binding, request).await };
let Some(Ok(response)) =
crate::ports::timeout(&*state.clock, probe, SIDECAR_PROBE_TIMEOUT).await
else {
return missing;
};
let contract = response
.headers()
.get(crate::sidecar::X_HARNESS_API)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u32>().ok());
let body = serde_json::from_slice::<serde_json::Value>(response.body()).ok();
let module = response
.headers()
.get(crate::sidecar::X_HARNESS_MODULE)
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
.or_else(|| first_module_string(body.as_ref(), "name"));
let version = first_module_string(body.as_ref(), "version");
let tables = declared_tables(body.as_ref());
let contract = contract.or_else(|| {
body.as_ref()
.and_then(|b| b.get("harness_api"))
.and_then(serde_json::Value::as_u64)
.and_then(|api| u32::try_from(api).ok())
});
missing["contract"] = contract.map_or(serde_json::Value::Null, |api| json!(api));
missing["module"] = module.map_or(serde_json::Value::Null, |name| json!(name));
missing["version"] = version.map_or(serde_json::Value::Null, |version| json!(version));
missing["tables"] = json!(tables);
match contract {
Some(api) if api != HARNESS_API => missing["probe"] = json!("mismatch"),
Some(_) => missing["probe"] = json!("ok"),
None => missing["probe"] = json!("unreachable"),
}
let clashes = collisions_with(&tables, state, &mount.name);
if !clashes.is_empty() {
let detail = format!(
"sidecar `{}` claims table(s) {} already claimed in this deployment",
mount.name,
clashes.join(", ")
);
tracing::error!(sidecar = %mount.name, tables = %clashes.join(","), "{detail}");
crate::logging::forward_internal_error(&detail);
missing["probe"] = json!("table-collision");
missing["table_collisions"] = json!(clashes);
}
missing
}
fn declared_tables(body: Option<&serde_json::Value>) -> Vec<String> {
let Some(modules) = body
.and_then(|b| b.get("modules"))
.and_then(|m| m.as_array())
else {
return Vec::new();
};
let mut tables: Vec<String> = modules
.iter()
.filter_map(|module| module.get("tables")?.as_array())
.flatten()
.filter_map(|table| table.as_str().map(str::to_owned))
.collect();
tables.sort_unstable();
tables.dedup();
tables
}
fn collisions_with(claimed: &[String], state: &HealthState, mount: &str) -> Vec<String> {
let mut clashes: Vec<String> = Vec::new();
for module in &state.modules {
if module.name() == mount {
continue;
}
for table in module.tables() {
if claimed.iter().any(|claimed| claimed == table)
&& !clashes.iter().any(|seen| seen == table)
{
clashes.push(format!("`{table}` (also module `{}`)", module.name()));
}
}
}
clashes
}
fn first_module_string(body: Option<&serde_json::Value>, key: &str) -> Option<String> {
body?
.get("modules")?
.get(0)?
.get(key)?
.as_str()
.map(str::to_owned)
}
struct TenantLayer {
routing: Option<Arc<dyn crate::tenant::TenantRouting>>,
fallback: Option<Arc<dyn Database>>,
}
async fn resolve_tenant_layer(
State(layer): State<Arc<TenantLayer>>,
scope: Scope,
mut request: axum::extract::Request,
next: Next,
) -> Response {
let host = request
.headers()
.get(header::HOST)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_owned();
let resolution = match layer.routing.as_ref() {
Some(routing) => routing.resolve(&host),
None => <crate::tenant::ImplicitTenant as crate::tenant::ResolveTenant>::resolve(
&crate::tenant::ImplicitTenant,
&host,
),
};
let tenant = match resolution.admit() {
Ok(tenant) => tenant,
Err(problem) => {
return Problem::new(problem)
.instance(&scope.request_id)
.into_response();
}
};
let db = match layer.routing.as_ref() {
Some(routing) => match routing.database(&tenant).await {
Ok(db) => Some(db),
Err(err) => {
tracing::error!(tenant = %tenant.id(), "{err}");
crate::logging::forward_internal_error(&err.to_string());
return Problem::new(&SLUGS.tenant_degraded)
.instance(&scope.request_id)
.into_response();
}
},
None => layer.fallback.clone(),
};
if let Some(db) = db {
let tenancy = if layer.routing.is_some() {
crate::tenant::Tenancy::FromRegistry
} else {
crate::tenant::Tenancy::Sole
};
request
.extensions_mut()
.insert(crate::tenant_conn::ResolvedTenant {
tenant,
tenancy,
db,
});
}
next.run(request).await
}
#[derive(Clone)]
struct ReadyState {
db: Option<Arc<dyn Database>>,
clock: Arc<dyn Clock>,
missing_ports: Vec<Port>,
}
fn missing_module_ports(modules: &[Arc<dyn Module>], ports: &Ports) -> Vec<Port> {
const PROBED: &[Port] = &[Port::VectorIndex, Port::Embedder];
PROBED
.iter()
.copied()
.filter(|port| {
modules
.iter()
.any(|module| module.requires().contains(port))
&& !ports.has(*port)
})
.collect()
}
async fn ready_handler(State(state): State<ReadyState>) -> impl IntoResponse {
if !state.missing_ports.is_empty() {
let detail = state
.missing_ports
.iter()
.map(|port| format!("{} port is not configured", port.name()))
.collect::<Vec<_>>()
.join("; ");
return Problem::not_ready(detail).into_response();
}
let Some(db) = state.db else {
return Problem::not_ready("database port is not configured").into_response();
};
let stmt = Statement::new("SELECT 1");
let query = async move { db.query(&stmt).await };
match crate::ports::timeout(&*state.clock, query, Duration::from_secs(2)).await {
Some(Ok(_rows)) => Json(json!({ "ok": true })).into_response(),
Some(Err(err)) => {
error!(error = %err, "readiness probe query failed");
crate::logging::forward_internal_error(&format!("readiness probe query failed: {err}"));
Problem::not_ready("database query failed").into_response()
}
None => Problem::not_ready("database did not answer within 2 s").into_response(),
}
}
#[derive(Clone)]
struct SurfaceState {
config: Arc<dyn Config>,
source: Arc<dyn SurfaceSource>,
}
struct MergedSurface {
base: Arc<SurfaceVariants>,
mounts: Vec<SidecarMount>,
dispatcher: Option<Arc<dyn Dispatcher>>,
gateway: Option<Arc<HmacSigner>>,
env: VentureEnv,
captcha_configured: bool,
}
impl MergedSurface {
async fn sidecar_modules(&self) -> Vec<crate::surface::ModuleSurface> {
let mut extra = Vec::new();
let Some(dispatcher) = &self.dispatcher else {
return extra;
};
for mount in &self.mounts {
if !dispatcher.has(&mount.binding) {
continue;
}
let mut builder = axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri("/__surface")
.header(header::ACCEPT, "application/json");
if let Some(signer) = &self.gateway {
builder =
builder.header(X_HARNESS_GATEWAY, mint_gateway(signer, &mount.name, false));
}
let request = builder
.body(bytes::Bytes::new())
.expect("static request builds");
let answer = match dispatcher.dispatch(&mount.binding, request).await {
Ok(response) if response.status().is_success() => response,
Ok(response) => {
tracing::warn!(module = mount.name, status = %response.status(), "sidecar surface not available");
continue;
}
Err(err) => {
tracing::warn!(module = mount.name, error = %err, "sidecar surface fetch failed");
continue;
}
};
if answer.body().len() > MAX_SIDECAR_SURFACE_BYTES {
tracing::warn!(
module = mount.name,
"sidecar surface exceeds {MAX_SIDECAR_SURFACE_BYTES} bytes and was not merged"
);
continue;
}
match serde_json::from_slice::<SurfaceDocument>(answer.body()) {
Ok(document) => match sanitize_sidecar_document(&document, &mount.name) {
Ok(entries) => extra.extend(entries.into_iter().filter_map(|mut entry| {
if self.env == VentureEnv::Production
&& !self.captcha_configured
&& strip_unguarded_captcha_actions(&mut entry.surface) > 0
{
tracing::warn!(
module = entry.name,
"merged sidecar actions requiring a captcha were dropped: no Captcha port here"
);
}
(!entry.surface.is_empty()).then_some(entry)
})),
Err(problems) => {
for problem in problems {
tracing::warn!(module = mount.name, error = problem, "sidecar surface rejected at merge");
}
}
},
Err(err) => {
tracing::warn!(module = mount.name, error = %err, "sidecar surface is not a surface document");
}
}
}
extra
}
}
#[async_trait::async_trait]
impl SurfaceSource for MergedSurface {
async fn current(&self) -> Arc<SurfaceDocument> {
if self.mounts.is_empty() {
return Arc::clone(&self.base.document);
}
let mut document = (*self.base.document).clone();
document.modules.extend(self.sidecar_modules().await);
Arc::new(document)
}
fn built(&self) -> Arc<SurfaceDocument> {
Arc::clone(&self.base.document)
}
fn rendered(&self, admin: bool) -> Option<&RenderedSurface> {
Some(if admin {
&self.base.full
} else {
&self.base.public
})
}
}
async fn surface_handler(
State(state): State<SurfaceState>,
headers: HeaderMap,
) -> impl IntoResponse {
let admin = require_admin(&*state.config, &headers).is_ok();
let current = state.source.current().await;
let built = state.source.built();
let prerendered = Arc::ptr_eq(¤t, &built).then(|| state.source.rendered(admin));
let fresh;
let rendered: &RenderedSurface = if let Some(rendered) = prerendered.flatten() {
rendered
} else {
fresh = if admin {
RenderedSurface::render(¤t)
} else {
RenderedSurface::render(¤t.public())
};
&fresh
};
let matches = headers
.get(header::IF_NONE_MATCH)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| {
value
.split(',')
.map(str::trim)
.any(|tag| tag == "*" || tag == rendered.etag)
});
let mut response = if matches {
StatusCode::NOT_MODIFIED.into_response()
} else {
(
[(header::CONTENT_TYPE, "application/json")],
rendered.json.clone(),
)
.into_response()
};
let response_headers = response.headers_mut();
response_headers.insert(
header::ETAG,
header::HeaderValue::from_str(&rendered.etag).expect("hex etag is a valid header"),
);
response_headers.insert(
header::CACHE_CONTROL,
if admin {
header::HeaderValue::from_static("private, no-store")
} else {
header::HeaderValue::from_static("no-cache")
},
);
response_headers.insert(
header::VARY,
header::HeaderValue::from_static("Authorization"),
);
response
}
#[derive(Default)]
pub struct HarnessBuilder {
venture: Option<Venture>,
modules: Vec<Arc<dyn Module>>,
provides: Vec<Port>,
runtime: Option<Arc<dyn Runtime>>,
module_templates: Vec<(String, Box<dyn Template>)>,
overrides: Vec<(String, Box<dyn Template>)>,
ui: Option<Arc<dyn UiMount>>,
actor_handlers: Vec<(String, Arc<dyn ActorHandler>)>,
}
impl HarnessBuilder {
#[must_use]
pub fn venture(mut self, venture: Venture) -> Self {
self.venture = Some(venture);
self
}
#[must_use]
pub fn module(mut self, module: impl Module) -> Self {
self.modules.push(Arc::new(module));
self
}
#[must_use]
pub fn module_arc(mut self, module: Arc<dyn Module>) -> Self {
self.modules.push(module);
self
}
#[must_use]
pub fn runtime(mut self, runtime: impl Runtime) -> Self {
let runtime: Arc<dyn Runtime> = Arc::new(runtime);
self.provides = runtime.provides();
self.runtime = Some(runtime);
self
}
#[must_use]
pub fn templates(
mut self,
templates: impl IntoIterator<Item = (String, Box<dyn Template>)>,
) -> Self {
self.module_templates.extend(templates);
self
}
#[must_use]
pub fn ui(mut self, ui: impl UiMount) -> Self {
self.ui = Some(Arc::new(ui));
self
}
#[must_use]
pub fn template(mut self, id: impl Into<String>, template: Box<dyn Template>) -> Self {
self.overrides.push((id.into(), template));
self
}
#[must_use]
pub fn actor(mut self, kind: &str, handler: Arc<dyn ActorHandler>) -> Self {
self.actor_handlers.push((kind.to_owned(), handler));
self
}
pub fn build(self) -> Result<Harness, ConfigError> {
let mut errors = ConfigError::default();
let venture = if let Some(venture) = self.venture {
venture.validate(&mut errors);
venture
} else {
errors.push("missing venture: call .venture(Venture::new(..)) before .build()");
Venture::new("invalid", "invalid.invalid")
};
let mut names: HashMap<&'static str, usize> = HashMap::new();
let mut tables: HashMap<&'static str, &'static str> = HashMap::new();
for module in &self.modules {
if module.harness_api() != HARNESS_API {
errors.push(harness_api_mismatch(module.as_ref()));
}
let name = module.name();
if name.is_empty() || !is_module_name(name) {
errors.push(format!(
"module name `{name}` must be kebab-case ([a-z0-9]+ separated by '-')"
));
}
if names.insert(name, 1).is_some() {
errors.push(format!("duplicate module name `{name}`"));
}
for port in module.requires().iter().chain(module.optional()) {
if !Port::ALL.contains(port) {
errors.push(format!(
"module `{name}` declares unknown port {}",
port.name()
));
}
}
for port in module.requires() {
if module.optional().contains(port) {
errors.push(format!(
"module `{name}` lists port {} in both requires() and optional()",
port.name()
));
}
}
module.surface().validate(name, &mut errors);
check_tables(module.as_ref(), &mut tables, &mut errors);
}
let actor_handlers = collect_actors(&self.actor_handlers, &self.modules, &mut errors);
let well_known = collect_well_known(&self.modules, &mut errors);
for module in &self.modules {
for port in module.requires() {
if !self.provides.contains(port) {
errors.push(format!(
"module `{}` requires port {} which the runtime does not provide",
module.name(),
port.name()
));
}
}
warn_undeclared_ports(module.as_ref(), &self.provides);
}
let ordered = resolve_dependency_order(&self.modules, &mut errors);
append_production_readiness(&venture, &self.modules, self.runtime.as_ref(), &mut errors);
check_template_ids(
self.overrides.iter().chain(self.module_templates.iter()),
&names,
&mut errors,
);
let surface = Arc::new(SurfaceVariants::compose(
&venture,
&self.modules,
self.ui.as_ref(),
));
if let Some(ui) = &self.ui {
ui.validate(&surface.document, &mut errors);
}
errors.into_result()?;
let mut registry = TemplateRegistry::new();
registry.register_all(self.module_templates);
registry.register_all(self.overrides);
let mut events = EventBus::new();
for module in &self.modules {
for (name, handler) in module.events() {
events = events.on(name, handler);
}
}
Ok(Harness {
venture: Arc::new(venture),
unprotected_acceptance_recorded: AtomicBool::new(false),
unlimited_acceptance_recorded: AtomicBool::new(false),
personal_data: Arc::new(crate::PersonalDataCatalog::compose(
ordered.iter().map(|m| (m.name(), m.personal_data())),
)),
modules: ordered,
templates: Arc::new(registry),
events,
runtime: self.runtime,
well_known,
surface,
ui: self.ui,
actor_handlers,
})
}
}
fn resolve_dependency_order(
modules: &[Arc<dyn Module>],
errors: &mut ConfigError,
) -> Vec<Arc<dyn Module>> {
let names: BTreeSet<&str> = modules.iter().map(|module| module.name()).collect();
let mut unknown = false;
for module in modules {
for needed in module.depends_on() {
if !names.contains(needed) {
unknown = true;
errors.push(format!(
"module `{}` depends on `{needed}`, which this venture does not compose: \
add the module or drop the dependency",
module.name()
));
}
}
}
if unknown {
return modules.to_vec();
}
let mut remaining: Vec<Option<Arc<dyn Module>>> = modules
.iter()
.map(|module| Some(Arc::clone(module)))
.collect();
let mut placed: BTreeSet<&str> = BTreeSet::new();
let mut ordered: Vec<Arc<dyn Module>> = Vec::with_capacity(modules.len());
while ordered.len() < modules.len() {
let mut progressed = false;
for slot in &mut remaining {
let ready = slot.as_ref().is_some_and(|module| {
module
.depends_on()
.iter()
.all(|needed| placed.contains(needed))
});
if ready {
let module = slot.take().expect("checked above");
placed.insert(module.name());
ordered.push(module);
progressed = true;
}
}
if !progressed {
let stuck: Vec<&str> = remaining
.iter()
.filter_map(|slot| slot.as_ref().map(|module| module.name()))
.collect();
errors.push(format!(
"modules [{}] depend on each other in a cycle: migrations cannot be ordered so \
that every module follows the ones it sits on top of",
stuck.join(", ")
));
return modules.to_vec();
}
}
ordered
}
fn check_template_ids<'a>(
ids: impl Iterator<Item = &'a (String, Box<dyn Template>)>,
names: &HashMap<&'static str, usize>,
errors: &mut ConfigError,
) {
for (id, _) in ids {
let Some(module_name) = id.split('/').next() else {
continue;
};
if !names.contains_key(module_name) {
errors.push(format!(
"template `{id}` names module `{module_name}` which is not registered"
));
}
}
}
fn is_module_name(name: &str) -> bool {
name.split('-').all(|part| {
!part.is_empty()
&& part
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
})
}
async fn production_readiness_guard(
State(problems): State<Arc<Vec<String>>>,
request: axum::extract::Request,
next: Next,
) -> Response {
if problems.is_empty() || !request.uri().path().starts_with("/v1/") {
return next.run(request).await;
}
let problem = Problem::new(&SLUGS.not_production_ready).with_detail(problems.join("; "));
match request.extensions().get::<Scope>() {
Some(scope) => problem.instance(&scope.request_id.clone()),
None => problem,
}
.into_response()
}
async fn admin_rate_limit_layer(
State(limiter): State<Option<Arc<dyn RateLimiter>>>,
request: axum::extract::Request,
next: Next,
) -> Response {
if !crate::sidecar::is_admin_path(request.uri().path()) {
return next.run(request).await;
}
let keys: Vec<String> = crate::rate_limit::rate_limit_keys(
crate::rate_limit::client_ip(request.headers()).as_deref(),
None,
)
.into_iter()
.map(|key| format!("admin:{key}"))
.collect();
if let RateLimit::Denied { decision } =
crate::rate_limit::check_rate_limit(limiter.as_ref(), &keys, RateLimitFailure::FailClosed)
.await
{
return rate_limited(&decision);
}
next.run(request).await
}
fn append_production_readiness(
venture: &Venture,
modules: &[Arc<dyn Module>],
runtime: Option<&Arc<dyn Runtime>>,
errors: &mut ConfigError,
) {
for error in crate::route_policy::production_readiness(
venture.env,
&crate::route_policy::WriteGuards::collect(modules),
runtime,
crate::route_policy::rate_limiter_effective(runtime),
crate::route_policy::signer_effective(runtime),
None,
None,
) {
errors.push(error);
}
}
fn collect_well_known(modules: &[Arc<dyn Module>], errors: &mut ConfigError) -> Option<Router> {
let mut providers: Vec<&'static str> = Vec::new();
let mut well_known = None;
for module in modules {
if let Some(router) = module.well_known() {
providers.push(module.name());
well_known = Some(router);
}
}
if providers.len() > 1 {
let listed = providers
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ");
errors.push(format!(
"modules {listed} all provide a well-known router; at most one module may occupy /.well-known"
));
}
well_known
}
#[cfg(test)]
mod acceptance_recording {
use super::*;
use crate::Migrations;
use crate::config::MapConfig;
struct PubWriter;
impl Module for PubWriter {
fn name(&self) -> &'static str {
"writer"
}
fn version(&self) -> &'static str {
"0.0.0-test"
}
fn requires(&self) -> &'static [Port] {
&[]
}
fn public_writes(&self) -> bool {
true
}
fn migrations(&self) -> Migrations {
Migrations::default()
}
fn validate_config(&self, _: &dyn Config) -> Result<(), ConfigError> {
Ok(())
}
fn router(&self, _: ModuleContext) -> Router {
Router::new()
}
}
struct AllPorts;
impl Runtime for AllPorts {
fn provides(&self) -> Vec<Port> {
Port::ALL.to_vec()
}
}
fn harness() -> Harness {
Harness::builder()
.venture(
Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
)
.module(PubWriter)
.runtime(AllPorts)
.build()
.expect("a development venture builds")
}
fn deployed(unlimited: Option<&str>) -> MapConfig {
let pairs = [("ENV", "production")].into_iter().chain(
unlimited.map(|reason| (crate::route_policy::ALLOW_UNLIMITED_PUBLIC_ROUTES, reason)),
);
MapConfig::from_pairs(pairs)
}
#[test]
fn the_waiver_is_recorded_once_and_only_when_it_waived() {
let harness = harness();
let config = deployed(Some("issue #437: limiter binding pending"));
assert!(
harness
.production_readiness_now(VentureEnv::Production, &config, false, true)
.is_empty(),
"the recorded acceptance serves"
);
assert!(
harness
.unlimited_acceptance_recorded
.load(Ordering::Relaxed)
);
assert!(
harness
.production_readiness_now(VentureEnv::Production, &config, false, true)
.is_empty()
);
}
#[test]
fn nothing_is_recorded_when_the_port_was_fine_all_along() {
let harness = harness();
let config = deployed(Some("unused: the limiter resolved"));
assert!(
harness
.production_readiness_now(VentureEnv::Production, &config, true, true)
.is_empty()
);
assert!(
!harness
.unlimited_acceptance_recorded
.load(Ordering::Relaxed),
"a waiver that removed no refusal is not a recorded acceptance"
);
}
#[test]
fn a_refusing_deployment_records_no_acceptance_at_all() {
let harness = harness();
let config = MapConfig::from_pairs([
("ENV", "production"),
(
crate::route_policy::ALLOW_UNPROTECTED_WRITES,
"captcha pending",
),
]);
assert_eq!(
harness
.production_readiness_now(VentureEnv::Production, &config, false, true)
.len(),
1
);
assert!(
!harness
.unprotected_acceptance_recorded
.load(Ordering::Relaxed)
);
assert!(
!harness
.unlimited_acceptance_recorded
.load(Ordering::Relaxed)
);
}
struct HmacWriter;
impl Module for HmacWriter {
fn name(&self) -> &'static str {
"webhooks"
}
fn version(&self) -> &'static str {
"0.0.0-test"
}
fn requires(&self) -> &'static [Port] {
&[]
}
fn public_writes(&self) -> bool {
true
}
fn public_write_policy(&self) -> crate::route_policy::RoutePolicy {
crate::route_policy::RoutePolicy::Signature
}
fn signature_verification(&self) -> crate::route_policy::SignatureVerification {
crate::route_policy::SignatureVerification::Hmac {
secret: "WEBHOOK_SECRET",
}
}
fn migrations(&self) -> Migrations {
Migrations::default()
}
fn validate_config(&self, _: &dyn Config) -> Result<(), ConfigError> {
Ok(())
}
fn router(&self, _: ModuleContext) -> Router {
Router::new()
}
}
#[test]
fn a_non_payments_signature_venture_boots_on_its_webhook_secret_alone() {
let harness = Harness::builder()
.venture(
Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
)
.module(HmacWriter)
.runtime(AllPorts)
.build()
.expect("a development venture builds");
let deployed = MapConfig::from_pairs([("ENV", "production")]);
let problems =
harness.production_readiness_now(VentureEnv::Production, &deployed, true, true);
assert_eq!(problems.len(), 1, "{problems:?}");
assert!(
problems[0].contains("WEBHOOKS_WEBHOOK_SECRET"),
"{}",
problems[0]
);
assert!(
!problems[0].contains("Payments"),
"a module verifying through the HMAC scheme is not the Payments leg's business: {}",
problems[0]
);
let ready = MapConfig::from_pairs([
("ENV", "production"),
("WEBHOOKS_WEBHOOK_SECRET", "whsec-test-dummy"),
]);
assert!(
harness
.production_readiness_now(VentureEnv::Production, &ready, true, true)
.is_empty()
);
}
}