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}