prodex 0.25.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use crate::secret_store::{
    SecretBackend, SecretError, SecretLocation, SecretRevision, SecretRevisionBackend, SecretValue,
};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct FileSecretBackend;

impl FileSecretBackend {
    pub fn new() -> Self {
        Self
    }
}

impl SecretBackend for FileSecretBackend {
    fn read(&self, location: &SecretLocation) -> Result<Option<SecretValue>, SecretError> {
        let path = file_path(location)?;
        let bytes = match fs::read(path) {
            Ok(bytes) => bytes,
            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
            Err(err) => return Err(SecretError::io(path, err)),
        };

        match String::from_utf8(bytes.clone()) {
            Ok(text) => Ok(Some(SecretValue::Text(text))),
            Err(_) => Ok(Some(SecretValue::Bytes(bytes))),
        }
    }

    fn write(&self, location: &SecretLocation, value: SecretValue) -> Result<(), SecretError> {
        let path = file_path(location)?;

        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|err| SecretError::io(parent, err))?;
        }

        let bytes = value.into_bytes();
        let temp_path = unique_temp_path(path);
        fs::write(&temp_path, bytes).map_err(|err| SecretError::io(&temp_path, err))?;
        replace_file(&temp_path, path)?;
        secure_file(path)?;
        Ok(())
    }

    fn delete(&self, location: &SecretLocation) -> Result<(), SecretError> {
        let path = file_path(location)?;

        match fs::remove_file(path) {
            Ok(()) => Ok(()),
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(err) => Err(SecretError::io(path, err)),
        }
    }
}

impl SecretRevisionBackend for FileSecretBackend {
    fn probe_revision(
        &self,
        location: &SecretLocation,
    ) -> Result<Option<SecretRevision>, SecretError> {
        let path = file_path(location)?;

        match fs::metadata(path) {
            Ok(metadata) => Ok(Some(SecretRevision::from_metadata(&metadata))),
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(err) => Err(SecretError::io(path, err)),
        }
    }
}

fn file_path(location: &SecretLocation) -> Result<&Path, SecretError> {
    match location {
        SecretLocation::File(path) => Ok(path),
        SecretLocation::Keyring { service, account } => Err(SecretError::unsupported(format!(
            "keyring://{service}/{account}"
        ))),
    }
}

fn unique_temp_path(path: &Path) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let pid = std::process::id();
    let temp_name = format!(
        "{}.{}.{}.tmp",
        path.file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("secret"),
        pid,
        nanos
    );
    path.with_file_name(temp_name)
}

fn replace_file(temp_path: &Path, path: &Path) -> Result<(), SecretError> {
    match fs::rename(temp_path, path) {
        Ok(()) => Ok(()),
        Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
            fs::remove_file(path).map_err(|err| SecretError::io(path, err))?;
            fs::rename(temp_path, path).map_err(|err| SecretError::io(path, err))
        }
        Err(err) => Err(SecretError::io(path, err)),
    }
}

fn secure_file(path: &Path) -> Result<(), SecretError> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let permissions = fs::Permissions::from_mode(0o600);
        fs::set_permissions(path, permissions).map_err(|err| SecretError::io(path, err))?;
    }
    Ok(())
}