1use std::collections::{BTreeMap, BTreeSet};
41use std::fmt;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
54pub enum RoutePolicy {
55 Requires,
59 CachePolicy,
61 Concurrency,
63 Resilience,
65}
66
67impl RoutePolicy {
68 pub const ALL: [RoutePolicy; 4] = [
72 RoutePolicy::Requires,
73 RoutePolicy::CachePolicy,
74 RoutePolicy::Concurrency,
75 RoutePolicy::Resilience,
76 ];
77
78 pub const fn field(self) -> &'static str {
81 match self {
82 RoutePolicy::Requires => "requires",
83 RoutePolicy::CachePolicy => "cache_policy",
84 RoutePolicy::Concurrency => "concurrency",
85 RoutePolicy::Resilience => "resilience",
86 }
87 }
88
89 pub const fn effect(self) -> &'static str {
92 match self {
93 RoutePolicy::Requires => "gate the route behind a resolved session",
94 RoutePolicy::CachePolicy => "cache the response for the declared ttl/swr",
95 RoutePolicy::Concurrency => "cap in-flight requests for this route",
96 RoutePolicy::Resilience => "retry and time-bound the render",
97 }
98 }
99
100 pub fn parse(field: &str) -> Option<Self> {
102 Self::ALL.into_iter().find(|p| p.field() == field)
103 }
104
105 pub const fn subfields(self) -> &'static [&'static str] {
120 match self {
121 RoutePolicy::CachePolicy => &["ttl", "swr", "negative_ttl", "vary"],
122 RoutePolicy::Resilience => &["retry", "timeout_ms"],
123 RoutePolicy::Requires | RoutePolicy::Concurrency => &[],
124 }
125 }
126
127 pub const fn reserved_subfields(self) -> &'static [&'static str] {
132 match self {
133 RoutePolicy::Resilience => &["queue"],
135 _ => &[],
136 }
137 }
138}
139
140impl fmt::Display for RoutePolicy {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.write_str(self.field())
143 }
144}
145
146pub const STRUCTURAL_FIELDS: [&str; 8] = [
155 "route",
157 "mode",
158 "render_entrypoint",
159 "source_reads",
161 "data_inputs",
162 "prerender",
164 "placement",
165 "hydration",
166];
167
168#[derive(Debug, Clone)]
174pub struct PolicySupport {
175 tier: String,
176 enforced: BTreeSet<RoutePolicy>,
177 delegated: BTreeSet<RoutePolicy>,
178}
179
180impl PolicySupport {
181 pub fn new(tier: impl Into<String>) -> Self {
185 Self {
186 tier: tier.into(),
187 enforced: BTreeSet::new(),
188 delegated: BTreeSet::new(),
189 }
190 }
191
192 #[must_use]
194 pub fn enforces(mut self, policy: RoutePolicy) -> Self {
195 self.enforced.insert(policy);
196 self
197 }
198
199 #[must_use]
205 pub fn delegate(mut self, policy: RoutePolicy) -> Self {
206 self.delegated.insert(policy);
207 self
208 }
209
210 pub fn tier(&self) -> &str {
211 &self.tier
212 }
213
214 pub fn covers(&self, policy: RoutePolicy) -> bool {
215 self.enforced.contains(&policy) || self.delegated.contains(&policy)
216 }
217
218 pub fn is_delegated(&self, policy: RoutePolicy) -> bool {
219 self.delegated.contains(&policy)
220 }
221
222 pub fn enforced(&self) -> impl Iterator<Item = RoutePolicy> + '_ {
224 self.enforced.iter().copied()
225 }
226
227 pub fn delegated(&self) -> impl Iterator<Item = RoutePolicy> + '_ {
229 self.delegated.iter().copied()
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum Violation {
236 Unenforced { route: String, policy: RoutePolicy },
238 UnknownField { route: String, field: String },
244}
245
246impl Violation {
247 fn route(&self) -> &str {
248 match self {
249 Violation::Unenforced { route, .. } | Violation::UnknownField { route, .. } => route,
250 }
251 }
252
253 fn line(&self) -> String {
254 match self {
255 Violation::Unenforced { route, policy } => format!(
256 " {route} declares `{}` — nothing here will {}",
257 policy.field(),
258 policy.effect(),
259 ),
260 Violation::UnknownField { route, field } => format!(
261 " {route} declares `{field}`, which nothing in this binary implements — the \
262 manifest is either newer than this build or uses a slot reserved for one"
263 ),
264 }
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct PolicyRefusal {
271 pub tier: String,
272 pub violations: Vec<Violation>,
273}
274
275impl fmt::Display for PolicyRefusal {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 let n = self.violations.len();
278 write!(
279 f,
280 "refusing to start: {n} route policy declaration(s) that `{}` does not enforce.\n",
281 self.tier,
282 )?;
283 for v in &self.violations {
284 writeln!(f, "{}", v.line())?;
285 }
286 let unknown = self
287 .violations
288 .iter()
289 .any(|v| matches!(v, Violation::UnknownField { .. }));
290 let known: BTreeSet<&'static str> = self
291 .violations
292 .iter()
293 .filter_map(|v| match v {
294 Violation::Unenforced { policy, .. } => Some(policy.field()),
295 Violation::UnknownField { .. } => None,
296 })
297 .collect();
298 write!(
299 f,
300 "A policy declared here and enforced nowhere is worse than no policy: the route \
301 serves 200 and looks correct. Either (a) drop the declaration if it was never \
302 meant to bind, (b) serve these routes on a tier that implements it, or (c) if \
303 something in front of this process really does enforce it, say so with \
304 `--policy-delegated <field>` / `MESOFACT_POLICY_DELEGATED=<field,…>`",
305 )?;
306 if !known.is_empty() {
307 write!(
308 f,
309 " (here: `{}`)",
310 known.into_iter().collect::<Vec<_>>().join(",")
311 )?;
312 }
313 f.write_str(".")?;
314 if unknown {
315 write!(
316 f,
317 " The unknown field(s) are not delegatable — upgrade this binary to one that \
318 knows them, or rebuild the workload with a matching toolchain."
319 )?;
320 }
321 Ok(())
322 }
323}
324
325impl std::error::Error for PolicyRefusal {}
326
327pub fn check_manifest(raw: &[u8], support: &PolicySupport) -> Result<(), PolicyCheckError> {
338 let doc: serde_json::Value =
339 serde_json::from_slice(raw).map_err(|e| PolicyCheckError::Unreadable(e.to_string()))?;
340 let routes = match doc.get("routes") {
341 Some(serde_json::Value::Array(routes)) => routes.as_slice(),
342 Some(_) => return Err(PolicyCheckError::Unreadable("`routes` is not an array".into())),
343 None => &[],
344 };
345 let mut violations = Vec::new();
346 for route in routes {
347 let Some(obj) = route.as_object() else {
348 return Err(PolicyCheckError::Unreadable(
349 "a manifest route entry is not an object".into(),
350 ));
351 };
352 let name = obj
353 .get("route")
354 .and_then(|v| v.as_str())
355 .unwrap_or("<unnamed route>")
356 .to_string();
357 for (key, value) in obj {
358 if STRUCTURAL_FIELDS.contains(&key.as_str()) {
359 continue;
360 }
361 match RoutePolicy::parse(key) {
362 Some(policy) => {
363 if is_declared(policy, value) && !support.covers(policy) {
364 violations.push(Violation::Unenforced {
365 route: name.clone(),
366 policy,
367 });
368 }
369 let known = policy.subfields();
374 if !known.is_empty() {
375 if let Some(obj) = value.as_object() {
376 for sub in obj.keys() {
377 if !known.contains(&sub.as_str()) {
378 violations.push(Violation::UnknownField {
379 route: name.clone(),
380 field: format!("{}.{sub}", policy.field()),
381 });
382 }
383 }
384 }
385 }
386 }
387 None => violations.push(Violation::UnknownField {
388 route: name.clone(),
389 field: key.clone(),
390 }),
391 }
392 }
393 }
394 if violations.is_empty() {
395 return Ok(());
396 }
397 violations.sort_by(|a, b| a.route().cmp(b.route()).then_with(|| a.line().cmp(&b.line())));
400 Err(PolicyCheckError::Refused(PolicyRefusal {
401 tier: support.tier.clone(),
402 violations,
403 }))
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
408pub enum PolicyCheckError {
409 Unreadable(String),
411 Refused(PolicyRefusal),
413}
414
415impl fmt::Display for PolicyCheckError {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 match self {
418 PolicyCheckError::Unreadable(why) => write!(
419 f,
420 "refusing to start: cannot read the route manifest to check for declared \
421 policy this binary does not enforce — {why}"
422 ),
423 PolicyCheckError::Refused(r) => r.fmt(f),
424 }
425 }
426}
427
428impl std::error::Error for PolicyCheckError {}
429
430fn is_declared(policy: RoutePolicy, value: &serde_json::Value) -> bool {
440 if value.is_null() {
441 return false;
442 }
443 match policy {
444 RoutePolicy::Requires => value.as_array().is_some_and(|a| !a.is_empty()),
445 RoutePolicy::CachePolicy => {
446 let Some(obj) = value.as_object() else {
447 return true;
450 };
451 obj.get("ttl").and_then(|v| v.as_u64()).unwrap_or(0) > 0
454 || obj.contains_key("swr")
455 || obj.contains_key("negative_ttl")
456 || obj
457 .get("vary")
458 .is_some_and(|v| v.as_array().is_some_and(|a| !a.is_empty()))
459 }
460 RoutePolicy::Concurrency => true,
461 RoutePolicy::Resilience => value
462 .as_object()
463 .is_some_and(|o| o.values().any(|v| !v.is_null())),
464 }
465}
466
467pub fn by_policy(refusal: &PolicyRefusal) -> BTreeMap<&'static str, Vec<&str>> {
470 let mut out: BTreeMap<&'static str, Vec<&str>> = BTreeMap::new();
471 for v in &refusal.violations {
472 let key = match v {
473 Violation::Unenforced { policy, .. } => policy.field(),
474 Violation::UnknownField { .. } => "<unknown>",
475 };
476 out.entry(key).or_default().push(v.route());
477 }
478 out
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::manifest::{
485 CachePolicy, Hydration, Prerender, Requires, ResiliencePolicy, ResolvedPlacement,
486 RetryPolicy, Route, RouteMode,
487 };
488
489 fn serve_tier() -> PolicySupport {
490 PolicySupport::new("mesofact serve").enforces(RoutePolicy::Resilience)
491 }
492
493 fn manifest(routes: &str) -> Vec<u8> {
494 format!(r#"{{"version":"1","build_id":"b","routes":[{routes}]}}"#).into_bytes()
495 }
496
497 const PLAIN: &str = r#"{"route":"/","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":0}}"#;
498
499 #[test]
505 fn a_declared_unenforced_policy_has_no_success_path() {
506 let tier = serve_tier();
507 let declarations = [
508 r#""requires":["user"]"#,
509 r#""cache_policy":{"ttl":3600}"#,
510 r#""cache_policy":{"ttl":0,"swr":60}"#,
511 r#""cache_policy":{"ttl":0,"vary":["accept-language"]}"#,
512 r#""concurrency":4"#,
513 r#""future_policy":{"limit":2}"#,
514 ];
515 for decl in declarations {
516 let raw = manifest(&format!(
517 r#"{{"route":"/p","mode":"ssr","render_entrypoint":"e.js","cache_policy":{{"ttl":0}},{decl}}}"#
518 ));
519 match check_manifest(&raw, &tier) {
520 Err(PolicyCheckError::Refused(_)) => {}
521 other => panic!(
522 "declaring {decl} on a tier that does not enforce it returned {other:?}; \
523 that is the silent no-op R749-T1 forbids ({} policies known)",
524 RoutePolicy::ALL.len(),
525 ),
526 }
527 }
528 }
529
530 #[test]
531 fn the_refusal_names_the_route_and_the_field() {
532 let raw = manifest(
533 r#"{"route":"/private","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"requires":["user"]}"#,
534 );
535 let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
536 assert!(err.contains("/private"), "{err}");
537 assert!(err.contains("requires"), "{err}");
538 assert!(err.contains("--policy-delegated"), "{err}");
539 }
540
541 #[test]
542 fn the_inert_cache_policy_every_route_carries_is_not_a_declaration() {
543 assert!(check_manifest(&manifest(PLAIN), &serve_tier()).is_ok());
544 }
545
546 #[test]
547 fn an_enforced_policy_passes_and_a_delegated_one_does_too() {
548 let raw = manifest(
549 r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":5000},"requires":["user"]}"#,
550 );
551 assert!(check_manifest(&raw, &serve_tier()).is_err());
552 let trusting = serve_tier().delegate(RoutePolicy::Requires);
553 assert!(check_manifest(&raw, &trusting).is_ok());
554 assert!(trusting.is_delegated(RoutePolicy::Requires));
555 assert!(!trusting.is_delegated(RoutePolicy::Resilience));
556 }
557
558 #[test]
562 fn an_empty_policy_block_is_not_a_declaration() {
563 let raw = manifest(
564 r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{},"requires":[]}"#,
565 );
566 assert!(check_manifest(&raw, &PolicySupport::new("bare")).is_ok());
567 }
568
569 #[test]
570 fn an_unparseable_manifest_is_not_a_pass() {
571 assert!(matches!(
572 check_manifest(b"{ not json", &serve_tier()),
573 Err(PolicyCheckError::Unreadable(_))
574 ));
575 assert!(matches!(
576 check_manifest(br#"{"routes":"nope"}"#, &serve_tier()),
577 Err(PolicyCheckError::Unreadable(_))
578 ));
579 }
580
581 #[test]
582 fn an_unknown_field_is_refused_and_not_delegatable() {
583 let raw = manifest(
584 r#"{"route":"/x","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"rate_limit":{"rps":10}}"#,
585 );
586 let mut permissive = serve_tier();
587 for p in RoutePolicy::ALL {
588 permissive = permissive.delegate(p);
589 }
590 let err = check_manifest(&raw, &permissive).unwrap_err().to_string();
591 assert!(err.contains("rate_limit"), "{err}");
592 assert!(err.contains("not delegatable"), "{err}");
593 }
594
595 #[test]
603 fn every_route_field_is_classified() {
604 let full = Route {
605 route: "/x/:id".into(),
606 mode: RouteMode::Ssr,
607 render_entrypoint: "dist/server/x.js".into(),
608 requires: Some(vec![Requires::User]),
609 source_reads: Some(vec!["s".into()]),
610 data_inputs: Some(vec!["d.json".into()]),
611 cache_policy: CachePolicy {
612 ttl: 1,
613 swr: Some(1),
614 negative_ttl: Some(1),
615 vary: Some(vec!["accept".into()]),
616 },
617 concurrency: Some(1),
618 hydration: Some(Hydration {
619 script: "s.js".into(),
620 code_split: vec![],
621 }),
622 prerender: Some(Prerender::Deferred { deferred: true }),
623 placement: Some(ResolvedPlacement::Host),
624 resilience: Some(ResiliencePolicy {
625 retry: Some(RetryPolicy {
626 attempts: 2,
627 backoff_ms: vec![10],
628 retry_on: None,
629 budget_ms: None,
630 }),
631 queue: None,
632 timeout_ms: Some(1),
633 }),
634 };
635 let json = serde_json::to_value(&full).unwrap();
636 let unclassified: Vec<&String> = json
637 .as_object()
638 .unwrap()
639 .keys()
640 .filter(|k| {
641 !STRUCTURAL_FIELDS.contains(&k.as_str()) && RoutePolicy::parse(k).is_none()
642 })
643 .collect();
644 assert!(
645 unclassified.is_empty(),
646 "manifest Route gained field(s) {unclassified:?} that are neither a RoutePolicy nor \
647 STRUCTURAL_FIELDS. Decide which: if a serving tier ignoring it would silently drop \
648 behaviour an author asked for, it is a RoutePolicy and every tier must advertise \
649 it; otherwise add it to STRUCTURAL_FIELDS with a reason.",
650 );
651 for policy in RoutePolicy::ALL {
654 let value = json
655 .get(policy.field())
656 .unwrap_or_else(|| panic!(
657 "RoutePolicy::{policy:?} names `{}`, which is not a field on manifest::Route",
658 policy.field(),
659 ));
660 let Some(obj) = value.as_object() else { continue };
664 let unclassified: Vec<&String> = obj
665 .keys()
666 .filter(|k| {
667 !policy.subfields().contains(&k.as_str())
668 && !policy.reserved_subfields().contains(&k.as_str())
669 })
670 .collect();
671 assert!(
672 unclassified.is_empty(),
673 "`{}` gained sub-field(s) {unclassified:?}: add them to RoutePolicy::subfields \
674 once a tier implements them, or to reserved_subfields with the doc that \
675 reserves the slot",
676 policy.field(),
677 );
678 }
679 }
680
681 #[test]
685 fn a_reserved_subfield_refuses_even_where_its_parent_policy_is_enforced() {
686 let raw = manifest(
687 r#"{"route":"/q","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":100,"queue":{"queue":"q","ack":"on_enqueue"}}}"#,
688 );
689 let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
690 assert!(err.contains("resilience.queue"), "{err}");
691 }
692
693 #[test]
694 fn an_unknown_cache_directive_refuses_instead_of_being_dropped_by_serde() {
695 let raw = manifest(
696 r#"{"route":"/c","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":60,"shared_max_age":30}}"#,
697 );
698 let tier = serve_tier().enforces(RoutePolicy::CachePolicy);
699 let err = check_manifest(&raw, &tier).unwrap_err().to_string();
700 assert!(err.contains("cache_policy.shared_max_age"), "{err}");
701 }
702}