Skip to main content

origin_mcp/
token.rs

1use std::path::Path;
2
3pub fn generate_token() -> String {
4    use base64::Engine;
5    use rand::RngExt;
6    let mut bytes = [0u8; 32];
7    rand::rng().fill(&mut bytes);
8    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
9}
10
11pub fn write_token(path: &Path, token: &str) -> anyhow::Result<()> {
12    if let Some(parent) = path.parent() {
13        std::fs::create_dir_all(parent)?;
14    }
15    std::fs::write(path, token)?;
16    #[cfg(unix)]
17    {
18        use std::os::unix::fs::PermissionsExt;
19        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
20    }
21    Ok(())
22}
23
24pub fn read_token(path: &Path) -> anyhow::Result<String> {
25    let content = std::fs::read_to_string(path)?;
26    Ok(content.trim().to_string())
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use std::fs;
33
34    #[test]
35    fn test_generate_token_is_43_chars_base64url() {
36        let token = generate_token();
37        assert_eq!(token.len(), 43, "token should be 43 base64url chars");
38        assert!(
39            token
40                .chars()
41                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
42            "token should only contain base64url chars: {token}"
43        );
44    }
45
46    #[test]
47    fn test_generate_token_is_unique() {
48        let t1 = generate_token();
49        let t2 = generate_token();
50        assert_ne!(t1, t2, "two generated tokens should differ");
51    }
52
53    #[test]
54    fn test_write_and_read_token_roundtrip() {
55        let dir = tempfile::tempdir().unwrap();
56        let path = dir.path().join("test-token");
57        let token = "test-token-value-abc123";
58        write_token(&path, token).unwrap();
59        let read_back = read_token(&path).unwrap();
60        assert_eq!(read_back, token);
61    }
62
63    #[test]
64    fn test_write_token_creates_parent_dirs() {
65        let dir = tempfile::tempdir().unwrap();
66        let path = dir.path().join("nested").join("dir").join("token");
67        let token = "abc";
68        write_token(&path, token).unwrap();
69        assert!(path.exists());
70        assert_eq!(read_token(&path).unwrap(), token);
71    }
72
73    #[test]
74    fn test_read_token_trims_whitespace() {
75        let dir = tempfile::tempdir().unwrap();
76        let path = dir.path().join("token");
77        fs::write(&path, "  my-token-value  \n").unwrap();
78        let token = read_token(&path).unwrap();
79        assert_eq!(token, "my-token-value");
80    }
81
82    #[test]
83    fn test_read_token_missing_file_errors() {
84        let result = read_token(Path::new("/nonexistent/path/token"));
85        assert!(result.is_err());
86    }
87
88    #[cfg(unix)]
89    #[test]
90    fn test_write_token_sets_0600_permissions() {
91        use std::os::unix::fs::PermissionsExt;
92        let dir = tempfile::tempdir().unwrap();
93        let path = dir.path().join("token");
94        write_token(&path, "secret").unwrap();
95        let perms = fs::metadata(&path).unwrap().permissions();
96        assert_eq!(perms.mode() & 0o777, 0o600, "token file should be 0600");
97    }
98}