pub const NOTIFICATION_TOKEN_HEADER: &str = "x-a2a-notification-token";
pub const NOTIFICATION_TOKEN_HEADER_UNPREFIXED: &str = "a2a-notification-token";
#[must_use]
pub fn notification_token(headers: &hyper::HeaderMap) -> Option<&str> {
let mut found: Option<&str> = None;
for name in [
NOTIFICATION_TOKEN_HEADER,
NOTIFICATION_TOKEN_HEADER_UNPREFIXED,
] {
for value in headers.get_all(name) {
let value = value.to_str().ok()?;
match found {
Some(seen) if seen != value => return None,
_ => found = Some(value),
}
}
}
found
}
pub(crate) fn origin_for_log(url: &str) -> String {
let Ok(uri) = url.parse::<hyper::Uri>() else {
return "<unparseable webhook url>".to_owned();
};
match (uri.scheme_str(), uri.host()) {
(Some(scheme), Some(host)) => uri.port_u16().map_or_else(
|| format!("{scheme}://{host}"),
|port| format!("{scheme}://{host}:{port}"),
),
_ => "<webhook url without scheme or host>".to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn headers(pairs: &[(&'static str, &'static str)]) -> hyper::HeaderMap {
let mut h = hyper::HeaderMap::new();
for (k, v) in pairs {
h.append(*k, v.parse().unwrap());
}
h
}
#[test]
fn reads_the_python_sdk_spelling() {
let h = headers(&[("X-A2A-Notification-Token", "t")]);
assert_eq!(notification_token(&h), Some("t"));
}
#[test]
fn reads_the_go_sdk_spelling() {
let h = headers(&[("A2A-Notification-Token", "t")]);
assert_eq!(notification_token(&h), Some("t"));
}
#[test]
fn reads_both_when_they_agree() {
let h = headers(&[
("x-a2a-notification-token", "t"),
("a2a-notification-token", "t"),
]);
assert_eq!(notification_token(&h), Some("t"));
}
#[test]
fn two_different_tokens_fail_closed() {
let across = headers(&[
("x-a2a-notification-token", "t"),
("a2a-notification-token", "u"),
]);
assert_eq!(notification_token(&across), None);
let repeated = headers(&[
("a2a-notification-token", "t"),
("a2a-notification-token", "u"),
]);
assert_eq!(notification_token(&repeated), None);
}
#[test]
fn absent_or_non_ascii_is_none() {
assert_eq!(notification_token(&hyper::HeaderMap::new()), None);
let mut h = hyper::HeaderMap::new();
h.insert(
NOTIFICATION_TOKEN_HEADER,
hyper::header::HeaderValue::from_bytes(b"caf\xc3\xa9").unwrap(),
);
assert_eq!(notification_token(&h), None);
}
#[test]
fn origin_for_log_drops_path_query_and_userinfo() {
assert_eq!(
origin_for_log("https://user:pw@hooks.example.com/t/SECRET?sig=SECRET"),
"https://hooks.example.com"
);
assert_eq!(
origin_for_log("http://127.0.0.1:8080/webhook/SECRET"),
"http://127.0.0.1:8080"
);
}
#[test]
fn origin_for_log_never_echoes_what_it_cannot_parse() {
assert_eq!(
origin_for_log("not a url SECRET"),
"<unparseable webhook url>"
);
assert_eq!(
origin_for_log("/relative/SECRET"),
"<webhook url without scheme or host>"
);
}
}