Skip to main content

cratestack_axum/envelope_layer/
mode.rs

1//! [`EnvelopeMode`] and the per-op [`EnvelopePolicy`] that picks it.
2
3use super::policy_request::PolicyRequest;
4
5/// How the layer treats one op's traffic (ADR 0006 §2).
6///
7/// `#[non_exhaustive]` (API-review decision, 2026-09-26): a later mode
8/// (say, "signed requests only, unsigned responses") must not be a breaking
9/// change for a policy that matches on this.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum EnvelopeMode {
13    /// Every request must be signed, `GET`, `HEAD` and `DELETE` included
14    /// (they seal an empty payload; decision D3). An unsigned one is refused
15    /// with the unsigned `401`. Every response is sealed.
16    ///
17    /// A signed `HEAD` still sends its COSE message as a request body,
18    /// which RFC 9110 §9.3.2 gives no semantics: hyper's server and
19    /// `reqwest` pass it through over HTTP/1.1, but an intermediary may
20    /// drop it (the request then fails verification, `401`) or refuse the
21    /// request. Prefer `GET` under `Required`.
22    Required,
23    /// A signed request is opened and its response is always sealed, as
24    /// under `Required` (decision S3). An unsigned one runs as before; its
25    /// response is sealed only when it carries a valid `Cratestack-Nonce`
26    /// and the [`super::ResponseSealPolicy`] asks for it (D10). A signed
27    /// request that fails verification is still the `401`, never downgraded
28    /// to unsigned.
29    Optional,
30    /// Plain traffic passes untouched. A request with a COSE body is still
31    /// refused (`415`) rather than forwarded unverified.
32    Off,
33}
34
35impl EnvelopeMode {
36    /// The stricter of two modes: `Required` over `Optional` over `Off`.
37    /// How a `/rpc/batch` call's frames combine (decision B1).
38    pub(super) fn strictest(self, other: Self) -> Self {
39        let rank = |mode: Self| match mode {
40            Self::Required => 2,
41            Self::Optional => 1,
42            Self::Off => 0,
43        };
44        if rank(other) > rank(self) {
45            other
46        } else {
47            self
48        }
49    }
50}
51
52/// Picks the [`EnvelopeMode`] for a request, from the op it addresses.
53///
54/// [`EnvelopeMode`] implements it (one mode for every op), and so does any
55/// `Fn(&PolicyRequest<'_>) -> EnvelopeMode`, so a per-op policy is a
56/// closure:
57///
58/// ```
59/// use cratestack_axum::envelope_layer::{EnvelopeMode, PolicyRequest};
60///
61/// let policy = |request: &PolicyRequest<'_>| {
62///     if request.is_subscription() { EnvelopeMode::Optional } else { EnvelopeMode::Required }
63/// };
64/// # let _ = policy;
65/// ```
66///
67/// There is no default: the layer's builder refuses to build without one
68/// (decision D9). A policy sees no header ([`PolicyRequest`]), so no
69/// request can talk it into a weaker mode.
70///
71/// **What the layer enforces whatever this returns:** a request with a
72/// COSE body is opened (or, under `Off`, refused) regardless; under
73/// `Required` no unsigned request reaches the router and no response leaves
74/// unsealed; a `/rpc/batch` call runs under the strictest mode of `batch`
75/// and every frame's op (decision B1). A policy can only choose how strict
76/// to be for plain traffic, never make the layer accept an unverified
77/// envelope.
78pub trait EnvelopePolicy: Send + Sync + 'static {
79    /// The mode for this op.
80    fn mode(&self, request: &PolicyRequest<'_>) -> EnvelopeMode;
81
82    /// The mode for traffic the layer cannot attribute to one op: a route
83    /// the router matched but the resolver cannot bind (a wrong mount
84    /// prefix, a hand-written route; decision S2), or a `/rpc/batch` body
85    /// whose frames it cannot read (B1). Under `Required` the first fails
86    /// closed with a `500` unless the route is allow-listed
87    /// ([`super::EnvelopeLayerBuilder::allow_unresolved`]), and the second
88    /// is refused. Under a non-`Required` answer, both pass through, and so
89    /// does a method the schema does not generate on a generated path
90    /// (otherwise the layer's `405`), unsigned.
91    ///
92    /// The default is `Required`, which fails closed: a per-op closure
93    /// cannot say what it would have answered for an op nobody can name.
94    /// An [`EnvelopeMode`] used as the policy answers itself. For a
95    /// closure, [`super::EnvelopeLayerBuilder::unresolved_mode`] replaces
96    /// it explicitly, without writing a type.
97    fn unresolved_mode(&self) -> EnvelopeMode {
98        EnvelopeMode::Required
99    }
100}
101
102impl EnvelopePolicy for EnvelopeMode {
103    fn mode(&self, _request: &PolicyRequest<'_>) -> EnvelopeMode {
104        *self
105    }
106
107    fn unresolved_mode(&self) -> EnvelopeMode {
108        *self
109    }
110}
111
112impl<F> EnvelopePolicy for F
113where
114    F: Fn(&PolicyRequest<'_>) -> EnvelopeMode + Send + Sync + 'static,
115{
116    fn mode(&self, request: &PolicyRequest<'_>) -> EnvelopeMode {
117        self(request)
118    }
119}
120
121/// A policy whose `unresolved_mode` the builder replaced
122/// ([`super::EnvelopeLayerBuilder::unresolved_mode`]).
123pub(super) struct WithUnresolved {
124    pub(super) policy: Box<dyn EnvelopePolicy>,
125    pub(super) unresolved: EnvelopeMode,
126}
127
128impl EnvelopePolicy for WithUnresolved {
129    fn mode(&self, request: &PolicyRequest<'_>) -> EnvelopeMode {
130        self.policy.mode(request)
131    }
132
133    fn unresolved_mode(&self) -> EnvelopeMode {
134        self.unresolved
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::EnvelopeMode::{Off, Optional, Required};
141
142    #[test]
143    fn strictest_is_symmetric_and_ranks_required_first() {
144        for (a, b, expected) in [
145            (Off, Optional, Optional),
146            (Optional, Required, Required),
147            (Off, Required, Required),
148            (Off, Off, Off),
149        ] {
150            assert_eq!(a.strictest(b), expected);
151            assert_eq!(b.strictest(a), expected);
152        }
153    }
154}