shared-context-engineering 0.2.0-pre-alpha-v2

Shared Context Engineering CLI
use std::fmt;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

use crate::services::auth::TokenResponse;
use crate::services::default_paths::resolve_sce_default_locations;

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StoredTokens {
    pub access_token: String,
    pub token_type: String,
    pub expires_in: u64,
    pub refresh_token: String,
    pub scope: Option<String>,
    pub stored_at_unix_seconds: u64,
}

impl StoredTokens {
    fn from_token_response(token: &TokenResponse) -> Result<Self, TokenStorageError> {
        let stored_at_unix_seconds = current_unix_timestamp_seconds()?;
        Ok(Self {
            access_token: token.access_token.clone(),
            token_type: token.token_type.clone(),
            expires_in: token.expires_in,
            refresh_token: token.refresh_token.clone(),
            scope: token.scope.clone(),
            stored_at_unix_seconds,
        })
    }
}

#[derive(Debug)]
pub enum TokenStorageError {
    PathResolution(String),
    Io(std::io::Error),
    Serialization(serde_json::Error),
    CorruptedTokenFile(String),
    #[allow(dead_code)]
    Permission(String),
}

impl fmt::Display for TokenStorageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::PathResolution(reason) => write!(
                f,
                "Unable to resolve token storage path: {reason}. Try: set a valid user home/state directory and retry."
            ),
            Self::Io(error) => write!(
                f,
                "Failed to read or write authentication tokens: {error}. Try: verify file permissions for the auth state directory."
            ),
            Self::Serialization(error) => write!(
                f,
                "Failed to serialize authentication tokens: {error}. Try: rerun login to regenerate credentials."
            ),
            Self::CorruptedTokenFile(reason) => write!(
                f,
                "Stored authentication tokens are invalid: {reason}. Try: run 'sce logout' and then 'sce login'."
            ),
            Self::Permission(reason) => write!(
                f,
                "Unable to apply secure token file permissions: {reason}. Try: verify local account permissions and retry."
            ),
        }
    }
}

impl std::error::Error for TokenStorageError {}

impl From<std::io::Error> for TokenStorageError {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<serde_json::Error> for TokenStorageError {
    fn from(value: serde_json::Error) -> Self {
        Self::Serialization(value)
    }
}

pub fn save_tokens(token: &TokenResponse) -> Result<StoredTokens, TokenStorageError> {
    let token_path = token_file_path()?;
    let stored = StoredTokens::from_token_response(token)?;
    save_tokens_at_path(&token_path, &stored)?;
    Ok(stored)
}

pub fn load_tokens() -> Result<Option<StoredTokens>, TokenStorageError> {
    let token_path = token_file_path()?;
    load_tokens_from_path(&token_path)
}

pub fn delete_tokens() -> Result<bool, TokenStorageError> {
    let token_path = token_file_path()?;
    delete_tokens_at_path(&token_path)
}

pub fn token_file_path() -> Result<PathBuf, TokenStorageError> {
    resolve_sce_default_locations()
        .map(|locations| locations.auth_tokens_file())
        .map_err(|error| TokenStorageError::PathResolution(error.to_string()))
}

fn save_tokens_at_path(path: &Path, stored: &StoredTokens) -> Result<(), TokenStorageError> {
    ensure_parent_directory(path)?;

    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(path)?;

    apply_secure_file_permissions(path)?;

    let encoded = serde_json::to_vec_pretty(stored)?;
    file.write_all(&encoded)?;
    file.write_all(b"\n")?;
    file.sync_all()?;

    Ok(())
}

fn load_tokens_from_path(path: &Path) -> Result<Option<StoredTokens>, TokenStorageError> {
    let content = match fs::read_to_string(path) {
        Ok(content) => content,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(error) => return Err(TokenStorageError::Io(error)),
    };

    let parsed: StoredTokens = serde_json::from_str(&content).map_err(|error| {
        TokenStorageError::CorruptedTokenFile(format!("{} ({error})", path.display()))
    })?;

    Ok(Some(parsed))
}

fn delete_tokens_at_path(path: &Path) -> Result<bool, TokenStorageError> {
    match fs::remove_file(path) {
        Ok(()) => Ok(true),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(error) => Err(TokenStorageError::Io(error)),
    }
}

fn ensure_parent_directory(path: &Path) -> Result<(), TokenStorageError> {
    let Some(parent) = path.parent() else {
        return Err(TokenStorageError::PathResolution(format!(
            "token path '{}' has no parent directory",
            path.display()
        )));
    };

    fs::create_dir_all(parent)?;
    apply_secure_directory_permissions(parent)?;
    Ok(())
}

fn current_unix_timestamp_seconds() -> Result<u64, TokenStorageError> {
    Ok(SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|error| {
            TokenStorageError::PathResolution(format!("system clock is invalid: {error}"))
        })?
        .as_secs())
}

#[cfg(unix)]
fn apply_secure_directory_permissions(path: &Path) -> Result<(), TokenStorageError> {
    use std::os::unix::fs::PermissionsExt;

    fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
    Ok(())
}

#[cfg(not(unix))]
fn apply_secure_directory_permissions(_path: &Path) -> Result<(), TokenStorageError> {
    Ok(())
}

#[cfg(unix)]
fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> {
    use std::os::unix::fs::PermissionsExt;

    fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
    Ok(())
}

#[cfg(windows)]
fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> {
    use std::process::Command;

    let username = std::env::var("USERNAME").map_err(|_| {
        TokenStorageError::Permission(
            "USERNAME environment variable is unavailable on Windows".to_string(),
        )
    })?;

    let grant_rule = format!("{username}:(R,W)");
    let output = Command::new("icacls")
        .arg(path)
        .arg("/inheritance:r")
        .arg("/grant:r")
        .arg(grant_rule)
        .output()
        .map_err(|error| {
            TokenStorageError::Permission(format!("failed to execute icacls: {error}"))
        })?;

    if output.status.success() {
        Ok(())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        Err(TokenStorageError::Permission(format!(
            "icacls failed for '{}': {stderr}",
            path.display()
        )))
    }
}

#[cfg(not(any(unix, windows)))]
fn apply_secure_file_permissions(_path: &Path) -> Result<(), TokenStorageError> {
    Ok(())
}