1mod activity;
20mod discovery;
21mod http;
22mod proxy;
23mod transport;
24
25pub use activity::Activity;
26pub use discovery::Discovery;
27pub use http::{HttpRequest, HttpResponse, parse_request};
28pub use proxy::{is_alive, proxy_streams};
29pub use transport::{HttpTransport, handle, post};
30
31pub const MCP_PATH: &str = "/mcp";
34
35#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Token(String);
42
43impl Token {
44 pub fn generate() -> Self {
46 let mut bytes = [0u8; 32];
49 getrandom::fill(&mut bytes).expect("the OS random source must be available");
50 let hex: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
51 Self(hex)
52 }
53
54 pub fn from_string(value: impl Into<String>) -> Self {
55 Self(value.into())
56 }
57
58 pub fn expose(&self) -> &str {
59 &self.0
60 }
61
62 pub fn matches(&self, candidate: &str) -> bool {
64 let expected = self.0.as_bytes();
65 let candidate = candidate.as_bytes();
66 if expected.len() != candidate.len() {
67 return false;
68 }
69 let mut diff = 0u8;
70 for (a, b) in expected.iter().zip(candidate.iter()) {
71 diff |= a ^ b;
72 }
73 diff == 0
74 }
75}
76
77impl std::fmt::Display for Token {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str("Token(***)")
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn a_generated_token_is_long_and_unpredictable() {
89 let first = Token::generate();
90 let second = Token::generate();
91
92 assert_eq!(first.expose().len(), 64);
93 assert_ne!(first, second);
94 }
95
96 #[test]
97 fn token_equality_is_exact() {
98 let token = Token::from_string("secret-value");
99
100 assert!(token.matches("secret-value"));
101 assert!(!token.matches("secret-valuX"));
102 assert!(!token.matches("secret"));
103 assert!(!token.matches(""));
104 }
105
106 #[test]
107 fn the_display_does_not_leak_the_token() {
108 let token = Token::from_string("super-secret");
109 assert_eq!(token.to_string(), "Token(***)");
110 }
111}