use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::time::Duration;
use crate::Server;
use tracing::info;
#[cfg(feature = "ssr")]
use tracing::warn;
const DEFAULT_SERVE_PORT: u16 = 3000;
#[derive(clap::Args, Debug)]
pub struct ServeArgs {
workload: Option<PathBuf>,
#[arg(long)]
bundle: Option<PathBuf>,
#[arg(long)]
listen: Option<SocketAddr>,
#[arg(long)]
idle_ttl: Option<u64>,
#[arg(long, default_value_t = DEFAULT_SERVE_PORT)]
port: u16,
#[arg(long, default_value = "0.0.0.0")]
host: IpAddr,
#[arg(long)]
revalidate: bool,
#[arg(long, default_value = "mesofact.config.toml")]
publish_config: PathBuf,
#[arg(long, env = "MESOFACT_MIRROR_KEY")]
mirror_key: Option<String>,
#[arg(long = "allow-route", value_name = "ROUTE")]
allow_route: Vec<String>,
#[arg(long, conflicts_with_all = ["workload", "publish_config", "allow_route"])]
tenants: Option<PathBuf>,
#[arg(
long,
env = "MESOFACT_TRUST_EDGE_AUTH",
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
value_parser = parse_truthy,
)]
trust_edge_auth: bool,
}
fn parse_truthy(raw: &str) -> Result<bool, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"" | "0" | "false" | "no" | "off" => Ok(false),
"1" | "true" | "yes" | "on" => Ok(true),
other => Err(format!(
"expected a boolean (1/true/yes/on or 0/false/no/off), got {other:?}"
)),
}
}
impl ServeArgs {
fn bind_addr(&self) -> SocketAddr {
self.listen
.unwrap_or_else(|| SocketAddr::new(self.host, self.port))
}
}
pub async fn run(args: ServeArgs) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("mesofact=info,mesofact_dev=info,tower_http=info")
}),
)
.init();
if let Some(bundle) = args.bundle.as_ref() {
let bundle_abs = bundle.canonicalize().unwrap_or_else(|_| bundle.clone());
let server = Server::from_bundle(&bundle_abs)?;
assert_declared_auth_is_enforced(&bundle_abs.join("app"), args.trust_edge_auth)?;
let server = attach_bundle_ssr(server, &bundle_abs).await?;
let idle_ttl = args.idle_ttl.filter(|s| *s > 0).map(Duration::from_secs);
let listener = match socket_activation_listener()? {
Some(l) => {
info!(bundle = %bundle_abs.display(), "mesofact serve: serving bundle on inherited LISTEN_FDS socket");
l
}
None => {
let addr = args.bind_addr();
info!(%addr, bundle = %bundle_abs.display(), "mesofact-serve listening (bundle, static v0)");
tokio::net::TcpListener::bind(addr).await?
}
};
return server.serve_on_listener(listener, idle_ttl).await;
}
run_workload_modes(args).await
}
fn assert_declared_auth_is_enforced(
workload: &std::path::Path,
trust_edge_auth: bool,
) -> anyhow::Result<()> {
let gated = crate::routes_requiring_user(workload).map_err(|e| {
anyhow::anyhow!(
"refusing to start: cannot read the route manifest under {} to check for \
declared-authed routes: {e}",
workload.display(),
)
})?;
if gated.is_empty() {
return Ok(());
}
if trust_edge_auth {
info!(
routes = ?gated,
"serving routes that declare requires:[\"user\"] — this process does NOT enforce \
that gate; --trust-edge-auth asserts an authenticating edge is in front",
);
return Ok(());
}
anyhow::bail!(
"refusing to start: {} route(s) declare `requires: [\"user\"]` but `mesofact serve` \
does not enforce it — {}. That check lives only in `mesofact proxy`'s router, so \
serving these here would expose a confidential surface to anything that reaches the \
port. Either front this process with an authenticating edge (passway cheers-verify) \
and pass `--trust-edge-auth` / `MESOFACT_TRUST_EDGE_AUTH=1` to say so, or drop \
`requires` from the route if it was never meant to be gated.",
gated.len(),
gated.join(", "),
);
}
#[cfg(feature = "ssr")]
async fn attach_bundle_ssr(server: crate::Server, bundle: &std::path::Path) -> anyhow::Result<crate::Server> {
use crate::{ssr, SsrSpawnOptions};
let app = bundle.join("app");
let opts = SsrSpawnOptions::new(app.clone(), app.join("dist"), app.join(".mesofact-serve"))
.with_env(std::env::vars().collect());
Ok(match ssr::spawn(opts).await? {
Some(child) => {
info!(prefixes = ?child.prefixes(), "mesofact serve --bundle: ssr runtime attached");
server.with_ssr(child)
}
None => server,
})
}
#[cfg(not(feature = "ssr"))]
async fn attach_bundle_ssr(server: crate::Server, bundle: &std::path::Path) -> anyhow::Result<crate::Server> {
let app = bundle.join("app");
let ssr_routes = crate::routes_declaring_ssr(&app).map_err(|e| {
anyhow::anyhow!(
"refusing to start: cannot read the route manifest under {} to check for \
mode:\"ssr\" routes: {e}",
app.display(),
)
})?;
if ssr_routes.is_empty() {
return Ok(server);
}
anyhow::bail!(
"refusing to start: {} route(s) declare mode:\"ssr\" ({}) but this `mesofact serve` \
binary was built without the `ssr` feature (no V8) — it can only serve them as a 404. \
Serve this bundle with an ssr-enabled build (the shipped stock runtime is built \
`--features deploy`), or drop the ssr route(s) if this bundle is meant to be static.",
ssr_routes.len(),
ssr_routes.join(", "),
);
}
#[cfg(unix)]
fn socket_activation_listener() -> anyhow::Result<Option<tokio::net::TcpListener>> {
use std::os::fd::FromRawFd;
let n_fds: i32 = std::env::var("LISTEN_FDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if n_fds < 1 {
return Ok(None);
}
if let Ok(pid) = std::env::var("LISTEN_PID") {
if pid.parse::<u32>().ok() != Some(std::process::id()) {
return Ok(None);
}
}
const SD_LISTEN_FDS_START: i32 = 3;
let std_listener = unsafe { std::net::TcpListener::from_raw_fd(SD_LISTEN_FDS_START) };
std_listener
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("set_nonblocking on inherited LISTEN_FDS socket: {e}"))?;
let listener = tokio::net::TcpListener::from_std(std_listener)
.map_err(|e| anyhow::anyhow!("adopting inherited LISTEN_FDS socket: {e}"))?;
Ok(Some(listener))
}
#[cfg(not(unix))]
fn socket_activation_listener() -> anyhow::Result<Option<tokio::net::TcpListener>> {
Ok(None)
}
#[cfg(feature = "ssr")]
async fn run_workload_modes(args: ServeArgs) -> anyhow::Result<()> {
use crate::{revalidate, ssr, tenants, SsrSpawnOptions};
let addr = args.bind_addr();
if let Some(tenants_dir) = args.tenants.as_ref() {
if !args.revalidate {
warn!("--tenants implies the revalidate receiver; running multi-tenant receiver");
}
if args.mirror_key.is_some() {
warn!(
"--mirror-key / MESOFACT_MIRROR_KEY is ignored in --tenants mode — \
each tenant's bearer comes from its own mirror_key_env",
);
}
let files = tenants::load_tenants(tenants_dir)?;
let resolved = tenants::resolve_tenants(files, |name| std::env::var(name).ok());
let registry = tenants::TenantRegistry::new(resolved);
if registry.is_empty() {
anyhow::bail!(
"--tenants {} contains no tenants/<id>.toml files — a receiver with an \
empty registry rejects every poke",
tenants_dir.display()
);
}
registry.validate()?;
info!(tenants = registry.len(), dir = %tenants_dir.display(), "multi-tenant revalidate receiver");
return tenants::serve(registry, addr.ip(), addr.port()).await;
}
if args.revalidate {
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("--revalidate needs a <workload> dir (or use --tenants)"))?;
let workload_abs = workload.canonicalize().unwrap_or(workload);
return revalidate::serve(
revalidate::RevalidateConfig {
workload: workload_abs,
publish_config: args.publish_config,
mirror_key: args.mirror_key,
routes: args.allow_route,
},
addr.ip(),
addr.port(),
)
.await;
}
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("a <workload> dir is required for the SSR host (or --bundle for static serving)"))?;
let server = Server::from_workload(&workload)?;
let workload_abs = workload.canonicalize().unwrap_or(workload);
assert_declared_auth_is_enforced(&workload_abs, args.trust_edge_auth)?;
let opts = SsrSpawnOptions::new(
workload_abs.clone(),
workload_abs.join("dist"),
workload_abs.join(".mesofact-serve"),
)
.with_env(std::env::vars().collect());
let server = match ssr::spawn(opts).await? {
Some(child) => {
info!(prefixes = ?child.prefixes(), "mesofact-serve ssr runtime attached");
server.with_ssr(child)
}
None => {
warn!("mesofact-serve: no SSR routes (or no manifest) — serving static only");
server
}
};
let addr = args.bind_addr();
info!(%addr, workload = %workload_abs.display(), "mesofact-serve listening");
server.serve_on(addr).await
}
#[cfg(not(feature = "ssr"))]
async fn run_workload_modes(args: ServeArgs) -> anyhow::Result<()> {
if args.revalidate || args.tenants.is_some() {
anyhow::bail!(
"--revalidate / --tenants need the `ssr` build feature (V8); this is a static-only build"
);
}
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("a <workload> dir is required (or --bundle to serve a W272 bundle)"))?;
let server = Server::from_workload(&workload)?;
assert_declared_auth_is_enforced(&workload, args.trust_edge_auth)?;
let addr = args.bind_addr();
info!(%addr, workload = %workload.display(), "mesofact-serve listening (static only, no ssr)");
server.serve_on(addr).await
}
#[cfg(test)]
mod tests {
use super::*;
fn workload_with_manifest(json: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let dist = dir.path().join("dist");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(dist.join("manifest.json"), json).unwrap();
dir
}
const AUTHED: &str = r#"{"routes":[{"route":"/","mode":"ssr","requires":["user"]}]}"#;
#[test]
fn a_declared_authed_route_refuses_to_start_without_an_asserted_edge() {
let dir = workload_with_manifest(AUTHED);
let err = assert_declared_auth_is_enforced(dir.path(), false)
.unwrap_err()
.to_string();
assert!(err.contains("refusing to start"), "{err}");
assert!(
err.contains('/'),
"the message must name the route the operator has to look at: {err}"
);
assert!(
err.contains("--trust-edge-auth"),
"and the remedy, or the operator has a refusal with no next move: {err}"
);
}
#[test]
fn an_asserted_edge_allows_the_declared_authed_route() {
let dir = workload_with_manifest(AUTHED);
assert!(assert_declared_auth_is_enforced(dir.path(), true).is_ok());
}
#[test]
fn a_workload_declaring_no_authed_route_starts_unchanged() {
let dir = workload_with_manifest(r#"{"routes":[{"route":"/","mode":"static"}]}"#);
assert!(assert_declared_auth_is_enforced(dir.path(), false).is_ok());
let empty = tempfile::tempdir().unwrap();
assert!(assert_declared_auth_is_enforced(empty.path(), false).is_ok());
}
#[test]
fn the_edge_assertion_accepts_the_truthy_forms_an_operator_will_type() {
for yes in ["1", "true", "TRUE", "yes", "on", " true "] {
assert_eq!(parse_truthy(yes), Ok(true), "{yes:?}");
}
for no in ["", "0", "false", "no", "off"] {
assert_eq!(parse_truthy(no), Ok(false), "{no:?}");
}
assert_eq!(parse_truthy(""), Ok(false));
assert!(parse_truthy("maybe").is_err());
}
#[test]
fn an_unreadable_manifest_refuses_to_start() {
let dir = workload_with_manifest("{ not json");
let err = assert_declared_auth_is_enforced(dir.path(), false)
.unwrap_err()
.to_string();
assert!(err.contains("refusing to start"), "{err}");
}
#[cfg(not(feature = "ssr"))]
#[tokio::test]
async fn a_bundle_declaring_ssr_refuses_to_start_on_a_static_only_build() {
let bundle_dir = tempfile::tempdir().unwrap();
let bundle = bundle_dir.path();
let app = bundle.join("app");
std::fs::create_dir_all(app.join("dist")).unwrap();
std::fs::write(
app.join("dist").join("manifest.json"),
r#"{"routes":[{"route":"/live","mode":"ssr"},{"route":"/","mode":"static"}]}"#,
)
.unwrap();
let server = crate::Server::from_workload(&app).unwrap();
let err = match attach_bundle_ssr(server, bundle).await {
Ok(_) => panic!("expected a refusal — bundle declares mode:\"ssr\""),
Err(e) => e.to_string(),
};
assert!(err.contains("refusing to start"), "{err}");
assert!(err.contains("/live"), "must name the route: {err}");
assert!(err.contains("ssr"), "and say why: {err}");
}
#[cfg(not(feature = "ssr"))]
#[tokio::test]
async fn a_static_only_bundle_starts_unchanged_on_a_static_only_build() {
let bundle_dir = tempfile::tempdir().unwrap();
let bundle = bundle_dir.path();
let app = bundle.join("app");
std::fs::create_dir_all(app.join("dist")).unwrap();
std::fs::write(
app.join("dist").join("manifest.json"),
r#"{"routes":[{"route":"/","mode":"static"}]}"#,
)
.unwrap();
let server = crate::Server::from_workload(&app).unwrap();
assert!(attach_bundle_ssr(server, bundle).await.is_ok());
}
}