origin-mcp 0.8.3

MCP server for Origin, the local-first personal agent memory layer
Documentation
use std::path::Path;

pub fn generate_token() -> String {
    use base64::Engine;
    use rand::RngExt;
    let mut bytes = [0u8; 32];
    rand::rng().fill(&mut bytes);
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}

pub fn write_token(path: &Path, token: &str) -> anyhow::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, token)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
    }
    Ok(())
}

pub fn read_token(path: &Path) -> anyhow::Result<String> {
    let content = std::fs::read_to_string(path)?;
    Ok(content.trim().to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn test_generate_token_is_43_chars_base64url() {
        let token = generate_token();
        assert_eq!(token.len(), 43, "token should be 43 base64url chars");
        assert!(
            token
                .chars()
                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
            "token should only contain base64url chars: {token}"
        );
    }

    #[test]
    fn test_generate_token_is_unique() {
        let t1 = generate_token();
        let t2 = generate_token();
        assert_ne!(t1, t2, "two generated tokens should differ");
    }

    #[test]
    fn test_write_and_read_token_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test-token");
        let token = "test-token-value-abc123";
        write_token(&path, token).unwrap();
        let read_back = read_token(&path).unwrap();
        assert_eq!(read_back, token);
    }

    #[test]
    fn test_write_token_creates_parent_dirs() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("nested").join("dir").join("token");
        let token = "abc";
        write_token(&path, token).unwrap();
        assert!(path.exists());
        assert_eq!(read_token(&path).unwrap(), token);
    }

    #[test]
    fn test_read_token_trims_whitespace() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("token");
        fs::write(&path, "  my-token-value  \n").unwrap();
        let token = read_token(&path).unwrap();
        assert_eq!(token, "my-token-value");
    }

    #[test]
    fn test_read_token_missing_file_errors() {
        let result = read_token(Path::new("/nonexistent/path/token"));
        assert!(result.is_err());
    }

    #[cfg(unix)]
    #[test]
    fn test_write_token_sets_0600_permissions() {
        use std::os::unix::fs::PermissionsExt;
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("token");
        write_token(&path, "secret").unwrap();
        let perms = fs::metadata(&path).unwrap().permissions();
        assert_eq!(perms.mode() & 0o777, 0o600, "token file should be 0600");
    }
}