From 2bee42d7ba0bf5ded4ed0815d772094d33f54103 Mon Sep 17 00:00:00 2001 From: Jeremiah Russell Date: Tue, 21 Oct 2025 07:31:45 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20feat:=20Add=20token=20export/imp?= =?UTF-8?q?ort=20for=20ephemeral=20environments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add token CLI subcommand with export/import operations - Enable OAuth2 token persistence across clean environments - Support for containers, CI/CD, and ephemeral compute workflows - Compress tokens with gzip and encode as base64 for env vars - Automatic token restoration from CULL_GMAIL_TOKEN_CACHE - Secure file permissions (600) on restored tokens - Add comprehensive error handling for token operations - Update dependencies: base64, flate2, serde_json This feature enables cull-gmail to run in ephemeral environments like Docker containers and CI/CD pipelines without re-authentication by exporting tokens once and restoring them via environment variables. --- Cargo.lock | 28 +++ Cargo.toml | 2 + src/cli/main.rs | 61 ++++++ src/cli/token_cli.rs | 506 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 9 + 5 files changed, 606 insertions(+) create mode 100644 src/cli/token_cli.rs diff --git a/Cargo.lock b/Cargo.lock index 895109e..b4e84c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -372,11 +381,13 @@ dependencies = [ name = "cull-gmail" version = "0.0.11" dependencies = [ + "base64", "chrono", "clap", "clap-verbosity-flag", "config", "env_logger", + "flate2", "futures", "google-gmail1", "httpmock", @@ -542,6 +553,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1243,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1769,6 +1791,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index 50844f2..86c02b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ serde_json = "1.0.145" thiserror = "2.0.17" tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "process"] } toml = "0.9.7" +base64 = "0.22" +flate2 = "1.0" [dev-dependencies] httpmock = "0.8" diff --git a/src/cli/main.rs b/src/cli/main.rs index bd28ab9..bf8246a 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -114,6 +114,7 @@ use clap::{Parser, Subcommand}; mod labels_cli; mod messages_cli; mod rules_cli; +mod token_cli; use config::Config; use cull_gmail::{ClientConfig, EolAction, GmailClient, Result, RuleProcessor, Rules}; @@ -122,6 +123,7 @@ use std::{env, error::Error as stdError}; use labels_cli::LabelsCli; use messages_cli::MessagesCli; use rules_cli::RulesCli; +use token_cli::{TokenCli, restore_tokens_from_string}; /// Main CLI application structure defining global options and subcommands. /// @@ -193,6 +195,13 @@ enum SubCmds { /// retention periods, label targeting, and automated actions. #[clap(name = "rules", display_order = 2)] Rules(RulesCli), + + /// Export and import OAuth2 tokens for ephemeral environments. + /// + /// Supports token export to compressed strings and automatic import from + /// environment variables for container deployments and CI/CD pipelines. + #[clap(name = "token", display_order = 4)] + Token(TokenCli), } /// CLI application entry point with comprehensive error handling and logging setup. @@ -274,6 +283,9 @@ async fn main() { async fn run(args: Cli) -> Result<()> { let (config, client_config) = get_config()?; + // Check for token restoration before client initialization + restore_tokens_if_available(&config, &client_config)?; + let mut client = GmailClient::new_with_config(client_config).await?; let Some(sub_command) = args.sub_command else { @@ -286,6 +298,12 @@ async fn run(args: Cli) -> Result<()> { SubCmds::Message(messages_cli) => messages_cli.run(&mut client).await, SubCmds::Labels(labels_cli) => labels_cli.run(client).await, SubCmds::Rules(rules_cli) => rules_cli.run(&mut client).await, + SubCmds::Token(token_cli) => { + // Token commands don't need an initialized client, just the config + // We need to get a fresh client_config since the original was moved + let (_, token_client_config) = get_config()?; + token_cli.run(&token_client_config).await + } } } @@ -386,6 +404,7 @@ fn get_config() -> Result<(Config, ClientConfig)> { .set_default("config_root", "h:.cull-gmail")? .set_default("rules", "rules.toml")? .set_default("execute", true)? + .set_default("token_cache_env", "CULL_GMAIL_TOKEN_CACHE")? .add_source(config::File::with_name( path.to_path_buf().to_str().unwrap(), )) @@ -464,6 +483,48 @@ async fn run_rules(client: &mut GmailClient, rules: Rules, execute: bool) -> Res Ok(()) } +/// Restores OAuth2 tokens from environment variable if available. +/// +/// This function checks if the token cache environment variable is set and, +/// if found, restores the token files before client initialization to enable +/// ephemeral environment workflows. +/// +/// # Arguments +/// +/// * `config` - Application configuration containing token environment variable name +/// * `client_config` - Client configuration containing token persistence path +/// +/// # Returns +/// +/// Returns `Result<()>` indicating success or failure. Non-critical errors +/// (like missing environment variables) are logged but don't cause failure. +/// +/// # Process +/// +/// 1. **Check Environment**: Look for configured token cache environment variable +/// 2. **Skip if Missing**: Continue normally if environment variable not set +/// 3. **Restore Tokens**: Decode and restore token files if variable present +/// 4. **Log Results**: Report restoration success or failures +/// +/// This function enables seamless token restoration for: +/// - Container deployments with injected token environment variables +/// - CI/CD pipelines with stored token secrets +/// - Ephemeral compute environments requiring periodic Gmail access +fn restore_tokens_if_available(config: &Config, client_config: &ClientConfig) -> Result<()> { + let token_env_var = config.get_string("token_cache_env") + .unwrap_or_else(|_| "CULL_GMAIL_TOKEN_CACHE".to_string()); + + if let Ok(token_data) = env::var(&token_env_var) { + log::info!("Found {} environment variable, restoring tokens", token_env_var); + restore_tokens_from_string(&token_data, client_config.persist_path())?; + log::info!("Tokens successfully restored from environment variable"); + } else { + log::debug!("No {} environment variable found, proceeding with normal token flow", token_env_var); + } + + Ok(()) +} + /// Executes the specified end-of-life action on messages for a Gmail label. /// /// This function performs the actual message operations (trash or delete) based on diff --git a/src/cli/token_cli.rs b/src/cli/token_cli.rs new file mode 100644 index 0000000..568c69f --- /dev/null +++ b/src/cli/token_cli.rs @@ -0,0 +1,506 @@ +//! # Token Management CLI Module +//! +//! This module provides CLI functionality for exporting and importing OAuth2 tokens +//! to support running the application in ephemeral environments like containers or CI/CD pipelines. +//! +//! ## Overview +//! +//! The token management system allows users to: +//! +//! - **Export tokens**: Extract current OAuth2 tokens to a compressed base64 string +//! - **Import tokens**: Recreate token files from environment variables +//! - **Ephemeral workflows**: Run in clean environments by restoring tokens from env vars +//! +//! ## Use Cases +//! +//! ### Container Deployments +//! ```bash +//! # Export tokens from development environment +//! cull-gmail token export +//! +//! # Set environment variable in container +//! docker run -e CULL_GMAIL_TOKEN_CACHE="" my-app +//! ``` +//! +//! ### CI/CD Pipelines +//! ```bash +//! # Store tokens as secret in CI system +//! cull-gmail token export > token.secret +//! +//! # Use in pipeline +//! export CULL_GMAIL_TOKEN_CACHE=$(cat token.secret) +//! cull-gmail messages list --query "older_than:30d" +//! ``` +//! +//! ### Periodic Jobs +//! ```bash +//! # One-time setup: export tokens +//! TOKENS=$(cull-gmail token export) +//! +//! # Recurring job: restore and use +//! export CULL_GMAIL_TOKEN_CACHE="$TOKENS" +//! cull-gmail rules run +//! ``` +//! +//! ## Security Considerations +//! +//! - **Token Sensitivity**: Exported tokens contain OAuth2 refresh tokens - treat as secrets +//! - **Environment Variables**: Use secure secret management for token storage +//! - **Expiration**: Tokens may expire and require re-authentication +//! - **Scope Limitations**: Exported tokens maintain original OAuth2 scope restrictions +//! +//! ## Token Format +//! +//! Exported tokens are compressed JSON structures containing: +//! - OAuth2 access tokens +//! - Refresh tokens +//! - Token metadata and expiration +//! - Encoded as base64 for environment variable compatibility + +use std::fs; +use std::path::Path; +use clap::Subcommand; +use base64::{Engine as _, engine::general_purpose::STANDARD as Base64Engine}; +use crate::{Result, ClientConfig}; +use cull_gmail::Error; + +/// Token management operations for ephemeral environments. +/// +/// This CLI subcommand provides functionality to export OAuth2 tokens to compressed +/// strings suitable for environment variables, and import them in clean environments +/// to avoid re-authentication flows. +/// +/// ## Subcommands +/// +/// - **export**: Export current tokens to stdout as base64-encoded string +/// - **import**: Import tokens from environment variable (typically automatic) +/// +/// ## Usage Examples +/// +/// ### Export Tokens +/// ```bash +/// # Export to stdout +/// cull-gmail token export +/// +/// # Export to file +/// cull-gmail token export > tokens.env +/// +/// # Export to environment variable +/// export MY_TOKENS=$(cull-gmail token export) +/// ``` +/// +/// ### Import Usage +/// ```bash +/// # Set environment variable +/// export CULL_GMAIL_TOKEN_CACHE="" +/// +/// # Run normally - tokens will be restored automatically +/// cull-gmail labels +/// ``` +#[derive(clap::Parser, Debug)] +pub struct TokenCli { + #[command(subcommand)] + command: TokenCommand, +} + +/// Available token management operations. +/// +/// Each operation handles different aspects of token lifecycle management +/// for ephemeral environment support. +#[derive(Subcommand, Debug)] +pub enum TokenCommand { + /// Export current OAuth2 tokens to a compressed string. + /// + /// This command reads the current token cache and outputs a base64-encoded, + /// compressed representation suitable for storage in environment variables + /// or CI/CD secret systems. + /// + /// ## Output Format + /// + /// The output is a single line containing a base64-encoded string that represents + /// the compressed JSON structure of all OAuth2 tokens and metadata. + /// + /// ## Examples + /// + /// ```bash + /// # Basic export + /// cull-gmail token export + /// + /// # Store in environment variable + /// export TOKENS=$(cull-gmail token export) + /// + /// # Save to file + /// cull-gmail token export > token.secret + /// ``` + Export, + + /// Import OAuth2 tokens from environment variable. + /// + /// This command is typically not called directly, as token import happens + /// automatically during client initialization when the CULL_GMAIL_TOKEN_CACHE + /// environment variable is present. + /// + /// ## Manual Import + /// + /// ```bash + /// # Set the environment variable + /// export CULL_GMAIL_TOKEN_CACHE="" + /// + /// # Import explicitly (usually automatic) + /// cull-gmail token import + /// ``` + Import, +} + +impl TokenCli { + /// Execute the token management command. + /// + /// This method dispatches to the appropriate token operation based on the + /// selected subcommand and handles the complete workflow for token export + /// or import operations. + /// + /// # Arguments + /// + /// * `client_config` - Client configuration containing token storage paths + /// + /// # Returns + /// + /// Returns `Result<()>` indicating success or failure of the token operation. + /// + /// # Errors + /// + /// - File I/O errors when reading or writing token files + /// - Serialization errors when processing token data + /// - Environment variable errors during import operations + pub async fn run(&self, client_config: &ClientConfig) -> Result<()> { + match &self.command { + TokenCommand::Export => export_tokens(client_config).await, + TokenCommand::Import => import_tokens(client_config).await, + } + } +} + +/// Export OAuth2 tokens to a compressed base64 string. +/// +/// This function reads the token cache directory, compresses all token files, +/// and outputs a base64-encoded string suitable for environment variable storage. +/// +/// # Arguments +/// +/// * `config` - Client configuration containing token persistence path +/// +/// # Returns +/// +/// Returns `Result<()>` with the base64 string printed to stdout, or an error +/// if token files cannot be read or processed. +/// +/// # Process Flow +/// +/// 1. **Read Token Directory**: Scan the OAuth2 token persistence directory +/// 2. **Collect Token Files**: Read all token-related files and metadata +/// 3. **Compress Data**: Use gzip compression on the JSON structure +/// 4. **Encode**: Convert to base64 for environment variable compatibility +/// 5. **Output**: Print the resulting string to stdout +/// +/// # Errors +/// +/// - `Error::TokenNotFound` - No token cache directory or files found +/// - I/O errors reading token files +/// - Serialization errors processing token data +async fn export_tokens(config: &ClientConfig) -> Result<()> { + let token_path = Path::new(config.persist_path()); + let mut token_data = std::collections::HashMap::new(); + + if token_path.is_file() { + // OAuth2 token is stored as a single file + let filename = token_path.file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| Error::FileIo("Invalid token filename".to_string()))?; + + let content = fs::read_to_string(&token_path) + .map_err(|e| Error::FileIo(format!("Failed to read token file: {}", e)))?; + + token_data.insert(filename.to_string(), content); + } else if token_path.is_dir() { + // Token directory with multiple files (legacy support) + for entry in fs::read_dir(token_path).map_err(|e| Error::FileIo(e.to_string()))? { + let entry = entry.map_err(|e| Error::FileIo(e.to_string()))?; + let path = entry.path(); + + if path.is_file() { + let filename = path.file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| Error::FileIo("Invalid filename in token cache".to_string()))?; + + let content = fs::read_to_string(&path) + .map_err(|e| Error::FileIo(format!("Failed to read token file {}: {}", filename, e)))?; + + token_data.insert(filename.to_string(), content); + } + } + } else { + return Err(Error::TokenNotFound(format!( + "Token cache not found: {}", + token_path.display() + ))); + } + + if token_data.is_empty() { + return Err(Error::TokenNotFound("No token data found in cache".to_string())); + } + + // Serialize to JSON + let json_data = serde_json::to_string(&token_data) + .map_err(|e| Error::SerializationError(format!("Failed to serialize token data: {}", e)))?; + + // Compress using flate2 + use flate2::write::GzEncoder; + use flate2::Compression; + use std::io::Write; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(json_data.as_bytes()) + .map_err(|e| Error::SerializationError(format!("Failed to compress token data: {}", e)))?; + let compressed_data = encoder.finish() + .map_err(|e| Error::SerializationError(format!("Failed to finalize compression: {}", e)))?; + + // Encode to base64 + let encoded = Base64Engine.encode(&compressed_data); + + // Output to stdout + println!("{}", encoded); + + Ok(()) +} + +/// Import OAuth2 tokens from environment variable. +/// +/// This function reads the CULL_GMAIL_TOKEN_CACHE environment variable, +/// decompresses the token data, and recreates the token cache files. +/// +/// # Arguments +/// +/// * `config` - Client configuration containing token persistence path +/// +/// # Returns +/// +/// Returns `Result<()>` indicating successful token restoration or an error +/// if the environment variable is missing or token data cannot be processed. +/// +/// # Process Flow +/// +/// 1. **Read Environment**: Get CULL_GMAIL_TOKEN_CACHE environment variable +/// 2. **Decode**: Base64 decode the token string +/// 3. **Decompress**: Gunzip the token data +/// 4. **Parse**: Deserialize JSON token structure +/// 5. **Recreate Files**: Write token files to cache directory +/// 6. **Set Permissions**: Ensure appropriate file permissions for security +/// +/// # Errors +/// +/// - `Error::TokenNotFound` - Environment variable not set +/// - Decoding/decompression errors for malformed token data +/// - I/O errors creating token files +pub async fn import_tokens(config: &ClientConfig) -> Result<()> { + let token_env = std::env::var("CULL_GMAIL_TOKEN_CACHE") + .map_err(|_| Error::TokenNotFound( + "CULL_GMAIL_TOKEN_CACHE environment variable not set".to_string() + ))?; + + restore_tokens_from_string(&token_env, config.persist_path())?; + + log::info!("Tokens successfully imported from environment variable"); + Ok(()) +} + +/// Restore token files from a compressed base64 string. +/// +/// This internal function handles the complete token restoration process, +/// including decoding, decompression, and file recreation. +/// +/// # Arguments +/// +/// * `token_string` - Base64-encoded compressed token data +/// * `persist_path` - Directory path where token files should be created +/// +/// # Returns +/// +/// Returns `Result<()>` indicating successful restoration or processing errors. +/// +/// # File Permissions +/// +/// Created token files are set to 600 (owner read/write only) for security. +pub fn restore_tokens_from_string(token_string: &str, persist_path: &str) -> Result<()> { + // Decode from base64 + let compressed_data = Base64Engine.decode(token_string.trim()) + .map_err(|e| Error::SerializationError(format!("Failed to decode base64 token data: {}", e)))?; + + // Decompress + use flate2::read::GzDecoder; + use std::io::Read; + + let mut decoder = GzDecoder::new(compressed_data.as_slice()); + let mut json_data = String::new(); + decoder.read_to_string(&mut json_data) + .map_err(|e| Error::SerializationError(format!("Failed to decompress token data: {}", e)))?; + + // Parse JSON + let token_files: std::collections::HashMap = serde_json::from_str(&json_data) + .map_err(|e| Error::SerializationError(format!("Failed to parse token JSON: {}", e)))?; + + let token_path = Path::new(persist_path); + + // Count files for logging + let file_count = token_files.len(); + + if file_count == 1 && token_files.keys().next().map(|k| k.as_str()) == token_path.file_name().and_then(|n| n.to_str()) { + // Single file case - write directly to the persist path + let content = token_files.into_values().next().unwrap(); + + // Create parent directory if needed + if let Some(parent) = token_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| Error::FileIo(format!("Failed to create token directory {}: {}", parent.display(), e)))?; + } + + fs::write(&token_path, &content) + .map_err(|e| Error::FileIo(format!("Failed to write token file: {}", e)))?; + + // Set secure permissions (600 - owner read/write only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&token_path) + .map_err(|e| Error::FileIo(format!("Failed to get file metadata: {}", e)))? + .permissions(); + perms.set_mode(0o600); + fs::set_permissions(&token_path, perms) + .map_err(|e| Error::FileIo(format!("Failed to set file permissions: {}", e)))?; + } + } else { + // Multiple files case - create directory structure + fs::create_dir_all(token_path) + .map_err(|e| Error::FileIo(format!("Failed to create token directory {}: {}", persist_path, e)))?; + + // Write token files + for (filename, content) in token_files { + let file_path = token_path.join(&filename); + fs::write(&file_path, &content) + .map_err(|e| Error::FileIo(format!("Failed to write token file {}: {}", filename, e)))?; + + // Set secure permissions (600 - owner read/write only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&file_path) + .map_err(|e| Error::FileIo(format!("Failed to get file metadata: {}", e)))? + .permissions(); + perms.set_mode(0o600); + fs::set_permissions(&file_path, perms) + .map_err(|e| Error::FileIo(format!("Failed to set file permissions: {}", e)))?; + } + } + } + + log::info!("Restored {} token files to {}", file_count, persist_path); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use std::collections::HashMap; + + #[test] + fn test_token_export_import_cycle() { + // Create a temporary directory structure + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let token_dir = temp_dir.path().join("gmail1"); + fs::create_dir_all(&token_dir).expect("Failed to create token dir"); + + // Create mock token files + let mut test_files = HashMap::new(); + test_files.insert("tokencache.json".to_string(), + r#"{"access_token":"test_access","refresh_token":"test_refresh"}"#.to_string()); + test_files.insert("metadata.json".to_string(), + r#"{"created":"2023-01-01","expires":"2023-12-31"}"#.to_string()); + + for (filename, content) in &test_files { + fs::write(token_dir.join(filename), content) + .expect("Failed to write test token file"); + } + + // Test export + let config = crate::ClientConfig::builder() + .with_client_id("test") + .with_config_path(temp_dir.path().to_str().unwrap()) + .build(); + + // Export tokens (this would normally print to stdout) + // We'll test the internal function instead + let result = tokio_test::block_on(export_tokens(&config)); + assert!(result.is_ok(), "Export should succeed"); + + // For full integration test, we would capture stdout and test import + // but that requires more complex setup with process isolation + } + + #[test] + fn test_restore_tokens_from_string() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let persist_path = temp_dir.path().join("gmail1").to_string_lossy().to_string(); + + // Create test data + let mut token_data = HashMap::new(); + token_data.insert("test.json".to_string(), r#"{"token":"value"}"#.to_string()); + + let json_str = serde_json::to_string(&token_data).unwrap(); + + // Compress + use flate2::write::GzEncoder; + use flate2::Compression; + use std::io::Write; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(json_str.as_bytes()).unwrap(); + let compressed = encoder.finish().unwrap(); + + // Encode + let encoded = Base64Engine.encode(&compressed); + + // Test restore + let result = restore_tokens_from_string(&encoded, &persist_path); + assert!(result.is_ok(), "Restore should succeed: {:?}", result); + + // Verify file was created + let restored_path = Path::new(&persist_path).join("test.json"); + assert!(restored_path.exists(), "Token file should be restored"); + + let restored_content = fs::read_to_string(restored_path).unwrap(); + assert_eq!(restored_content, r#"{"token":"value"}"#); + } + + #[test] + fn test_missing_token_directory() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let config = crate::ClientConfig::builder() + .with_client_id("test") + .with_config_path(temp_dir.path().join("nonexistent").to_str().unwrap()) + .build(); + + let result = tokio_test::block_on(export_tokens(&config)); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::TokenNotFound(_))); + } + + #[test] + fn test_invalid_base64_restore() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let persist_path = temp_dir.path().to_string_lossy().to_string(); + + let result = restore_tokens_from_string("invalid-base64!", &persist_path); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::SerializationError(_))); + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index e3a06d2..2fbef99 100644 --- a/src/error.rs +++ b/src/error.rs @@ -51,4 +51,13 @@ pub enum Error { /// Invalid message age specification #[error("Invalid message age: {0}")] InvalidMessageAge(String), + /// Token not found or missing + #[error("Token error: {0}")] + TokenNotFound(String), + /// File I/O error with context + #[error("File I/O error: {0}")] + FileIo(String), + /// Serialization/deserialization error + #[error("Serialization error: {0}")] + SerializationError(String), }