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())),
344 None => &[],
345 };
346 let mut violations = Vec::new();
347 for route in routes {
348 let Some(obj) = route.as_object() else {
349 return Err(PolicyCheckError::Unreadable(
350 "a manifest route entry is not an object".into(),
351 ));
352 };
353 let name = obj
354 .get("route")
355 .and_then(|v| v.as_str())
356 .unwrap_or("<unnamed route>")
357 .to_string();
358 for (key, value) in obj {
359 if STRUCTURAL_FIELDS.contains(&key.as_str()) {
360 continue;
361 }
362 match RoutePolicy::parse(key) {
363 Some(policy) => {
364 if is_declared(policy, value) && !support.covers(policy) {
365 violations.push(Violation::Unenforced {
366 route: name.clone(),
367 policy,
368 });
369 }
370 let known = policy.subfields();
375 if !known.is_empty() {
376 if let Some(obj) = value.as_object() {
377 for sub in obj.keys() {
378 if !known.contains(&sub.as_str()) {
379 violations.push(Violation::UnknownField {
380 route: name.clone(),
381 field: format!("{}.{sub}", policy.field()),
382 });
383 }
384 }
385 }
386 }
387 }
388 None => violations.push(Violation::UnknownField {
389 route: name.clone(),
390 field: key.clone(),
391 }),
392 }
393 }
394 }
395 if violations.is_empty() {
396 return Ok(());
397 }
398 violations.sort_by(|a, b| a.route().cmp(b.route()).then_with(|| a.line().cmp(&b.line())));
401 Err(PolicyCheckError::Refused(PolicyRefusal {
402 tier: support.tier.clone(),
403 violations,
404 }))
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
409pub enum PolicyCheckError {
410 Unreadable(String),
412 Refused(PolicyRefusal),
414}
415
416impl fmt::Display for PolicyCheckError {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 match self {
419 PolicyCheckError::Unreadable(why) => write!(
420 f,
421 "refusing to start: cannot read the route manifest to check for declared \
422 policy this binary does not enforce — {why}"
423 ),
424 PolicyCheckError::Refused(r) => r.fmt(f),
425 }
426 }
427}
428
429impl std::error::Error for PolicyCheckError {}
430
431fn is_declared(policy: RoutePolicy, value: &serde_json::Value) -> bool {
441 if value.is_null() {
442 return false;
443 }
444 match policy {
445 RoutePolicy::Requires => value.as_array().is_some_and(|a| !a.is_empty()),
446 RoutePolicy::CachePolicy => {
447 let Some(obj) = value.as_object() else {
448 return true;
451 };
452 obj.get("ttl").and_then(|v| v.as_u64()).unwrap_or(0) > 0
455 || obj.contains_key("swr")
456 || obj.contains_key("negative_ttl")
457 || obj
458 .get("vary")
459 .is_some_and(|v| v.as_array().is_some_and(|a| !a.is_empty()))
460 }
461 RoutePolicy::Concurrency => true,
462 RoutePolicy::Resilience => value
463 .as_object()
464 .is_some_and(|o| o.values().any(|v| !v.is_null())),
465 }
466}
467
468pub fn by_policy(refusal: &PolicyRefusal) -> BTreeMap<&'static str, Vec<&str>> {
471 let mut out: BTreeMap<&'static str, Vec<&str>> = BTreeMap::new();
472 for v in &refusal.violations {
473 let key = match v {
474 Violation::Unenforced { policy, .. } => policy.field(),
475 Violation::UnknownField { .. } => "<unknown>",
476 };
477 out.entry(key).or_default().push(v.route());
478 }
479 out
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::manifest::{
486 CachePolicy, Hydration, Prerender, Requires, ResiliencePolicy, ResolvedPlacement,
487 RetryPolicy, Route, RouteMode,
488 };
489
490 fn serve_tier() -> PolicySupport {
491 PolicySupport::new("mesofact serve").enforces(RoutePolicy::Resilience)
492 }
493
494 fn manifest(routes: &str) -> Vec<u8> {
495 format!(r#"{{"version":"1","build_id":"b","routes":[{routes}]}}"#).into_bytes()
496 }
497
498 const PLAIN: &str = r#"{"route":"/","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":0}}"#;
499
500 #[test]
506 fn a_declared_unenforced_policy_has_no_success_path() {
507 let tier = serve_tier();
508 let declarations = [
509 r#""requires":["user"]"#,
510 r#""cache_policy":{"ttl":3600}"#,
511 r#""cache_policy":{"ttl":0,"swr":60}"#,
512 r#""cache_policy":{"ttl":0,"vary":["accept-language"]}"#,
513 r#""concurrency":4"#,
514 r#""future_policy":{"limit":2}"#,
515 ];
516 for decl in declarations {
517 let raw = manifest(&format!(
518 r#"{{"route":"/p","mode":"ssr","render_entrypoint":"e.js","cache_policy":{{"ttl":0}},{decl}}}"#
519 ));
520 match check_manifest(&raw, &tier) {
521 Err(PolicyCheckError::Refused(_)) => {}
522 other => panic!(
523 "declaring {decl} on a tier that does not enforce it returned {other:?}; \
524 that is the silent no-op R749-T1 forbids ({} policies known)",
525 RoutePolicy::ALL.len(),
526 ),
527 }
528 }
529 }
530
531 #[test]
532 fn the_refusal_names_the_route_and_the_field() {
533 let raw = manifest(
534 r#"{"route":"/private","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"requires":["user"]}"#,
535 );
536 let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
537 assert!(err.contains("/private"), "{err}");
538 assert!(err.contains("requires"), "{err}");
539 assert!(err.contains("--policy-delegated"), "{err}");
540 }
541
542 #[test]
543 fn the_inert_cache_policy_every_route_carries_is_not_a_declaration() {
544 assert!(check_manifest(&manifest(PLAIN), &serve_tier()).is_ok());
545 }
546
547 #[test]
548 fn an_enforced_policy_passes_and_a_delegated_one_does_too() {
549 let raw = manifest(
550 r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":5000},"requires":["user"]}"#,
551 );
552 assert!(check_manifest(&raw, &serve_tier()).is_err());
553 let trusting = serve_tier().delegate(RoutePolicy::Requires);
554 assert!(check_manifest(&raw, &trusting).is_ok());
555 assert!(trusting.is_delegated(RoutePolicy::Requires));
556 assert!(!trusting.is_delegated(RoutePolicy::Resilience));
557 }
558
559 #[test]
563 fn an_empty_policy_block_is_not_a_declaration() {
564 let raw = manifest(
565 r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{},"requires":[]}"#,
566 );
567 assert!(check_manifest(&raw, &PolicySupport::new("bare")).is_ok());
568 }
569
570 #[test]
571 fn an_unparseable_manifest_is_not_a_pass() {
572 assert!(matches!(
573 check_manifest(b"{ not json", &serve_tier()),
574 Err(PolicyCheckError::Unreadable(_))
575 ));
576 assert!(matches!(
577 check_manifest(br#"{"routes":"nope"}"#, &serve_tier()),
578 Err(PolicyCheckError::Unreadable(_))
579 ));
580 }
581
582 #[test]
583 fn an_unknown_field_is_refused_and_not_delegatable() {
584 let raw = manifest(
585 r#"{"route":"/x","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"rate_limit":{"rps":10}}"#,
586 );
587 let mut permissive = serve_tier();
588 for p in RoutePolicy::ALL {
589 permissive = permissive.delegate(p);
590 }
591 let err = check_manifest(&raw, &permissive).unwrap_err().to_string();
592 assert!(err.contains("rate_limit"), "{err}");
593 assert!(err.contains("not delegatable"), "{err}");
594 }
595
596 #[test]
604 fn every_route_field_is_classified() {
605 let full = Route {
606 route: "/x/:id".into(),
607 mode: RouteMode::Ssr,
608 render_entrypoint: "dist/server/x.js".into(),
609 requires: Some(vec![Requires::User]),
610 source_reads: Some(vec!["s".into()]),
611 data_inputs: Some(vec!["d.json".into()]),
612 cache_policy: CachePolicy {
613 ttl: 1,
614 swr: Some(1),
615 negative_ttl: Some(1),
616 vary: Some(vec!["accept".into()]),
617 },
618 concurrency: Some(1),
619 hydration: Some(Hydration {
620 script: "s.js".into(),
621 code_split: vec![],
622 }),
623 prerender: Some(Prerender::Deferred { deferred: true }),
624 placement: Some(ResolvedPlacement::Host),
625 resilience: Some(ResiliencePolicy {
626 retry: Some(RetryPolicy {
627 attempts: 2,
628 backoff_ms: vec![10],
629 retry_on: None,
630 budget_ms: None,
631 }),
632 queue: None,
633 timeout_ms: Some(1),
634 }),
635 };
636 let json = serde_json::to_value(&full).unwrap();
637 let unclassified: Vec<&String> = json
638 .as_object()
639 .unwrap()
640 .keys()
641 .filter(|k| {
642 !STRUCTURAL_FIELDS.contains(&k.as_str()) && RoutePolicy::parse(k).is_none()
643 })
644 .collect();
645 assert!(
646 unclassified.is_empty(),
647 "manifest Route gained field(s) {unclassified:?} that are neither a RoutePolicy nor \
648 STRUCTURAL_FIELDS. Decide which: if a serving tier ignoring it would silently drop \
649 behaviour an author asked for, it is a RoutePolicy and every tier must advertise \
650 it; otherwise add it to STRUCTURAL_FIELDS with a reason.",
651 );
652 for policy in RoutePolicy::ALL {
655 let value = json
656 .get(policy.field())
657 .unwrap_or_else(|| panic!(
658 "RoutePolicy::{policy:?} names `{}`, which is not a field on manifest::Route",
659 policy.field(),
660 ));
661 let Some(obj) = value.as_object() else { continue };
665 let unclassified: Vec<&String> = obj
666 .keys()
667 .filter(|k| {
668 !policy.subfields().contains(&k.as_str())
669 && !policy.reserved_subfields().contains(&k.as_str())
670 })
671 .collect();
672 assert!(
673 unclassified.is_empty(),
674 "`{}` gained sub-field(s) {unclassified:?}: add them to RoutePolicy::subfields \
675 once a tier implements them, or to reserved_subfields with the doc that \
676 reserves the slot",
677 policy.field(),
678 );
679 }
680 }
681
682 #[test]
686 fn a_reserved_subfield_refuses_even_where_its_parent_policy_is_enforced() {
687 let raw = manifest(
688 r#"{"route":"/q","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":100,"queue":{"queue":"q","ack":"on_enqueue"}}}"#,
689 );
690 let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
691 assert!(err.contains("resilience.queue"), "{err}");
692 }
693
694 #[test]
695 fn an_unknown_cache_directive_refuses_instead_of_being_dropped_by_serde() {
696 let raw = manifest(
697 r#"{"route":"/c","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":60,"shared_max_age":30}}"#,
698 );
699 let tier = serve_tier().enforces(RoutePolicy::CachePolicy);
700 let err = check_manifest(&raw, &tier).unwrap_err().to_string();
701 assert!(err.contains("cache_policy.shared_max_age"), "{err}");
702 }
703}