Skip to main content

mesofact_core/proxy/
config.rs

1//! CLI configuration for the mesofact-proxy binary.
2
3use clap::Parser;
4use std::path::PathBuf;
5
6#[derive(Debug, Parser)]
7#[command(name = "mesofact-proxy", about = "mesofact tri-mode web proxy (axum)")]
8pub struct Config {
9    /// Local path to manifest.json (required).
10    #[arg(long, env = "MESOFACT_MANIFEST")]
11    pub manifest: PathBuf,
12
13    /// Bind address for the HTTP listener.
14    #[arg(long, default_value = "0.0.0.0:3000", env = "MESOFACT_BIND")]
15    pub bind: String,
16
17    /// Number of Bun workers to spawn (default = num_cpus / 2, min 1).
18    #[arg(long, env = "MESOFACT_WORKERS")]
19    pub workers: Option<usize>,
20
21    /// CDN base URL for Mode 1 redirect dispatch (e.g. https://cdn.yah.dev).
22    /// When set, Mode 1 routes 302-redirect to `{cdn_base_url}{path}`.
23    #[arg(long, env = "MESOFACT_CDN_BASE_URL")]
24    pub cdn_base_url: Option<String>,
25
26    /// Local dist/ directory for Mode 1 fallback (when CDN is not configured).
27    #[arg(long, env = "MESOFACT_FALLBACK_DIR")]
28    pub fallback_dir: Option<PathBuf>,
29
30    /// Path to the mesofact-worker entry script (bun entrypoint).
31    #[arg(
32        long,
33        env = "MESOFACT_WORKER_ENTRY",
34        default_value = "packages/mesofact-worker/src/worker.ts"
35    )]
36    pub worker_entry: PathBuf,
37
38    /// Path to `mesofact.config.toml`. Read for source generation tokens
39    /// (cache-key input 6). Missing file → generations resolve to a placeholder.
40    #[arg(long, env = "MESOFACT_SOURCES_CONFIG")]
41    pub sources_config: Option<PathBuf>,
42
43    /// Env var holding the HMAC key for `CookieSessionResolver`. When set,
44    /// Mode 2 sessions resolve from the session cookie; when unset, sessions
45    /// are disabled (`requires: ["user"]` routes always redirect/401).
46    #[arg(long, env = "MESOFACT_SESSION_SECRET_ENV")]
47    pub session_secret_env: Option<String>,
48
49    /// Session cookie name (default `mesofact_session`).
50    #[arg(long, env = "MESOFACT_SESSION_COOKIE", default_value = "mesofact_session")]
51    pub session_cookie: String,
52
53    /// Login URL for `requires: ["user"]` routes with no session. The proxy
54    /// 302s here with `?next=<original-url>`. Unset → 401 instead.
55    #[arg(long, env = "MESOFACT_LOGIN_URL")]
56    pub login_url: Option<String>,
57
58    /// Mode 2 LRU response-cache capacity (entries).
59    #[arg(long, env = "MESOFACT_CACHE_CAPACITY", default_value_t = 4096)]
60    pub cache_capacity: usize,
61
62    /// Assert that something in front of this process enforces a route policy
63    /// this tier does not (R749-T1). Comma-separated field names as written in
64    /// `mesofact.routes.ts`.
65    ///
66    /// The proxy tier's standing case is `resilience`: W181 puts retry/timeout
67    /// at the always-up edge (the CF Worker reads them from `SSR_RESILIENCE`),
68    /// and this process implements none of it. Deployed behind that Worker the
69    /// policy really is enforced — by the Worker — so the deployment says so
70    /// here. Run without one and the refusal is correct.
71    ///
72    /// Same spelling and semantics as `mesofact serve`'s flag on purpose: an
73    /// operator moving a workload between tiers should not have to learn a
74    /// second vocabulary for the same assertion.
75    #[arg(
76        long,
77        env = "MESOFACT_POLICY_DELEGATED",
78        value_delimiter = ',',
79        value_parser = parse_policy_field,
80    )]
81    pub policy_delegated: Vec<crate::policy::RoutePolicy>,
82}
83
84/// Parse one `--policy-delegated` field name. A typo is rejected rather than
85/// ignored: a delegation that silently does not apply is a fail-open with a
86/// flag in front of it.
87fn parse_policy_field(raw: &str) -> Result<crate::policy::RoutePolicy, String> {
88    crate::policy::RoutePolicy::parse(raw.trim()).ok_or_else(|| {
89        format!(
90            "unknown route policy {raw:?} — known policies are {}",
91            crate::policy::RoutePolicy::ALL
92                .iter()
93                .map(|p| p.field())
94                .collect::<Vec<_>>()
95                .join(", "),
96        )
97    })
98}
99
100impl Config {
101    /// What `mesofact proxy` implements, advertised (R749-T1). One place, and
102    /// each line has a code site behind it:
103    ///
104    ///   - `requires` — `proxy::router`'s `Requires::User` check, the only
105    ///     session-aware serving path in the system.
106    ///   - `cache_policy` — `proxy::cache::ResponseCache`, built in
107    ///     `cli::proxy::run`.
108    ///   - `concurrency` — the worker pool's per-route semaphore
109    ///     (`packages/mesofact-worker/src/pool.ts`, configured from the
110    ///     manifest at `worker.ts`'s `buildRouteHandlers`). W225 §2c records
111    ///     this row as unenforced everywhere; that is wrong, and the code above
112    ///     is why.
113    ///   - `resilience` — NOT enforced. Nothing in `mesofact-core` reads it;
114    ///     W181's only consumer is the CF Worker
115    ///     (`packages/mesofact-edge/src/router.ts`, from `SSR_RESILIENCE`).
116    ///     Delegate it when that Worker is in front.
117    pub fn policy_support(&self) -> crate::policy::PolicySupport {
118        use crate::policy::RoutePolicy;
119        let mut support = crate::policy::PolicySupport::new("mesofact proxy")
120            .enforces(RoutePolicy::Requires)
121            .enforces(RoutePolicy::CachePolicy)
122            .enforces(RoutePolicy::Concurrency);
123        for policy in &self.policy_delegated {
124            support = support.delegate(*policy);
125        }
126        support
127    }
128
129    pub fn worker_count(&self) -> usize {
130        self.workers
131            .unwrap_or_else(|| (num_cpus::get() / 2).max(1))
132    }
133}