use crate::project_config::ProjectConfig;
use securestore::{KeySource, SecretsManager};
use std::path::PathBuf;
fn secrets_dir(project: &ProjectConfig) -> PathBuf {
project.project_dir.join("secrets")
}
fn vault_path(project: &ProjectConfig, env_name: &str) -> PathBuf {
secrets_dir(project).join(format!("{}.json", env_name))
}
fn key_path(project: &ProjectConfig, env_name: &str) -> PathBuf {
secrets_dir(project).join(format!("{}.key", env_name))
}
pub fn load_store(project: &ProjectConfig, env_name: &str) -> eyre::Result<SecretsManager> {
let vault = vault_path(project, env_name);
if !vault.exists() {
eyre::bail!(
"Secrets vault not found at {}. Run: cufflink secrets init --env {}",
vault.display(),
env_name
);
}
if let Ok(key_b64) = std::env::var("CUFFLINK_SECRETS_KEY") {
let key_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, key_b64.trim())
.map_err(|e| eyre::eyre!("Failed to decode CUFFLINK_SECRETS_KEY: {}", e))?;
let tmp = tempfile::NamedTempFile::new()?;
std::fs::write(tmp.path(), &key_bytes)?;
let store = SecretsManager::load(vault, KeySource::Path(tmp.path()))
.map_err(|e| eyre::eyre!("Failed to load secrets vault: {}", e))?;
return Ok(store);
}
let key = key_path(project, env_name);
if !key.exists() {
eyre::bail!(
"Secrets key not found. Either:\n \
- Set CUFFLINK_SECRETS_KEY env var (base64-encoded)\n \
- Place key file at {}",
key.display()
);
}
let store = SecretsManager::load(vault, KeySource::Path(&key))
.map_err(|e| eyre::eyre!("Failed to load secrets vault: {}", e))?;
Ok(store)
}
pub fn init(env: Option<&str>) -> eyre::Result<()> {
let project =
ProjectConfig::find_and_load()?.ok_or_else(|| eyre::eyre!("No Cufflink.toml found"))?;
let env_name = resolve_env_name(&project, env)?;
let dir = secrets_dir(&project);
std::fs::create_dir_all(&dir)?;
let vault = vault_path(&project, &env_name);
let key = key_path(&project, &env_name);
if vault.exists() {
eyre::bail!("Secrets vault already exists at {}", vault.display());
}
let store = SecretsManager::new(KeySource::Csprng)
.map_err(|e| eyre::eyre!("Failed to create secrets vault: {}", e))?;
store
.export_key(&key)
.map_err(|e| eyre::eyre!("Failed to export key: {}", e))?;
store
.save_as(&vault)
.map_err(|e| eyre::eyre!("Failed to save vault: {}", e))?;
let gitignore = dir.join(".gitignore");
if !gitignore.exists() {
std::fs::write(&gitignore, "*.key\n")?;
}
println!("Secrets initialized for '{}':", env_name);
println!(" Vault: {}", vault.display());
println!(" Key: {}", key.display());
println!(" .gitignore created (key files excluded)");
println!();
println!("Add secrets with:");
println!(
" cufflink secrets set \"name\" \"value\" --env {}",
env_name
);
Ok(())
}
pub fn set(name: &str, value: &str, env: Option<&str>) -> eyre::Result<()> {
let project =
ProjectConfig::find_and_load()?.ok_or_else(|| eyre::eyre!("No Cufflink.toml found"))?;
let env_name = resolve_env_name(&project, env)?;
let mut store = load_store(&project, &env_name)?;
store.set(name, value);
store
.save()
.map_err(|e| eyre::eyre!("Failed to save vault: {}", e))?;
println!("Secret '{}' set for '{}'", name, env_name);
Ok(())
}
pub fn delete(name: &str, env: Option<&str>) -> eyre::Result<()> {
let project =
ProjectConfig::find_and_load()?.ok_or_else(|| eyre::eyre!("No Cufflink.toml found"))?;
let env_name = resolve_env_name(&project, env)?;
let mut store = load_store(&project, &env_name)?;
store
.remove(name)
.map_err(|e| eyre::eyre!("Secret '{}' not found: {}", name, e))?;
store
.save()
.map_err(|e| eyre::eyre!("Failed to save vault: {}", e))?;
println!("Secret '{}' deleted from '{}'", name, env_name);
Ok(())
}
pub fn list(env: Option<&str>, reveal: bool) -> eyre::Result<()> {
let project =
ProjectConfig::find_and_load()?.ok_or_else(|| eyre::eyre!("No Cufflink.toml found"))?;
let env_name = resolve_env_name(&project, env)?;
let store = load_store(&project, &env_name)?;
let names: Vec<&str> = store.keys().collect();
if names.is_empty() {
println!("No secrets stored for '{}'.", env_name);
} else {
println!("Secrets for '{}':", env_name);
for name in names {
if reveal {
let value: String = store
.get(name)
.map_err(|e| eyre::eyre!("Failed to read '{}': {}", name, e))?;
println!(" {} = {}", name, value);
} else {
println!(" {}", name);
}
}
}
Ok(())
}
fn resolve_env_name(project: &ProjectConfig, env: Option<&str>) -> eyre::Result<String> {
let name = env
.map(|s| s.to_string())
.or_else(|| project.service.default_env.clone())
.ok_or_else(|| {
eyre::eyre!(
"No environment specified. Use --env <name> or set default_env in Cufflink.toml"
)
})?;
Ok(name)
}