1use crate::sec::secret;
12
13pub fn resolve_headers(templates: &[(String, String)]) -> Result<Vec<(String, String)>, String> {
19 let env = |k: &str| std::env::var(k).ok();
20 templates
21 .iter()
22 .map(|(name, value)| Ok((name.clone(), secret::resolve(value, &env)?)))
23 .collect()
24}
25
26pub fn headers_resolvable(templates: &[(String, String)]) -> Result<(), String> {
29 resolve_headers(templates).map(|_| ())
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35 use std::io::Write;
36
37 #[test]
38 fn resolves_env_secret_in_header_value() {
39 unsafe { std::env::set_var("MCP_AUTH_TEST_TOKEN", "s3cr3t") };
41 let headers = resolve_headers(&[(
42 "Authorization".into(),
43 "Bearer {{secret:MCP_AUTH_TEST_TOKEN}}".into(),
44 )])
45 .unwrap();
46 assert_eq!(headers[0].0, "Authorization");
47 assert_eq!(headers[0].1, "Bearer s3cr3t");
48 unsafe { std::env::remove_var("MCP_AUTH_TEST_TOKEN") };
49 }
50
51 #[test]
52 fn resolves_secret_file() {
53 let mut f = tempfile::NamedTempFile::new().unwrap();
54 writeln!(f, "file-token").unwrap();
55 let tmpl = format!("Bearer {{{{secret-file:{}}}}}", f.path().to_str().unwrap());
56 let headers = resolve_headers(&[("x-api-key".into(), tmpl)]).unwrap();
57 assert_eq!(headers[0].1, "Bearer file-token");
58 }
59
60 #[test]
61 fn plain_header_passes_through() {
62 let headers = resolve_headers(&[("Accept".into(), "application/json".into())]).unwrap();
63 assert_eq!(headers[0].1, "application/json");
64 }
65
66 #[test]
67 fn missing_secret_is_an_error_without_the_value() {
68 let err = resolve_headers(&[(
69 "Authorization".into(),
70 "Bearer {{secret:DEFINITELY_UNSET_MCP_VAR}}".into(),
71 )])
72 .unwrap_err();
73 assert!(err.contains("DEFINITELY_UNSET_MCP_VAR"));
74 assert!(!err.contains("Bearer"), "the value must not leak: {err}");
75 }
76}