//! The forge events stream: every verified `issues` and `pull_request` //! delivery swarm-controller receives, kept for the notification router. //! //! The controller checks a delivery's HMAC and publishes its body unchanged //! on [`subject`], one subject per Forgejo event type (the //! `X-Forgejo-Event-Type` header: `issues`, `issue_assign`, `pull_request`, //! `pull_request_sync`, …). The router reads the stream with one durable //! consumer, so it holds no webhook secret and registers no hook. //! //! Three crates name these: the controller publishes, the router consumes, //! and the auth-callout responder grants each its side. use std::time::Duration; /// The stream the deliveries are kept in. pub const STREAM: &str = "SWARM_EVENTS"; /// Every subject [`STREAM`] captures sits below this. pub const SUBJECT_PREFIX: &str = "$SWARM.events.forge"; /// How long the stream keeps a delivery. pub const MAX_AGE: Duration = Duration::from_hours(7 * 24); /// The router's durable consumer on [`STREAM`]. pub const ROUTER_CONSUMER: &str = "notify-router"; /// The subject a delivery of Forgejo event type `event_type` is kept under. #[must_use] pub fn subject(event_type: &str) -> String { format!("{SUBJECT_PREFIX}.{event_type}") } /// Every [`subject`] at once: the stream's subjects, the router's consumer /// filter and the controller's publish grant. #[must_use] pub fn subject_wildcard() -> String { subject("*") } /// The event type a [`subject`] names, or `None` for a subject not below /// [`SUBJECT_PREFIX`]. #[must_use] pub fn event_type(subject: &str) -> Option<&str> { subject .strip_prefix(SUBJECT_PREFIX)? .strip_prefix('.') .filter(|t| !t.is_empty() && !t.contains('.')) } /// Whether `event_type` can be one token of a subject. The value comes from /// a request header, and a `.`, a wildcard or whitespace would publish /// somewhere other than one leaf below [`SUBJECT_PREFIX`]. #[must_use] pub fn is_subject_token(event_type: &str) -> bool { !event_type.is_empty() && event_type .bytes() .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') } #[cfg(test)] mod tests { use super::*; #[test] fn a_subject_round_trips_to_its_event_type() { assert_eq!(subject("issue_assign"), "$SWARM.events.forge.issue_assign"); assert_eq!(subject_wildcard(), "$SWARM.events.forge.*"); assert_eq!( event_type("$SWARM.events.forge.issue_assign"), Some("issue_assign") ); assert_eq!(event_type("$SWARM.events.forge."), None); assert_eq!(event_type("$SWARM.events.forge.a.b"), None); assert_eq!(event_type("$SWARM.term.atlas"), None); } #[test] fn only_a_plain_event_type_is_a_subject_token() { for good in ["issues", "issue_assign", "pull_request_review_approved"] { assert!(is_subject_token(good), "{good}"); } for bad in ["", "a.b", "*", ">", "a b", "Issues"] { assert!(!is_subject_token(bad), "{bad:?}"); } } }