use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
const PREFERRED_SECRET_FILE_NAMES: &[&str] = &[
"github.env",
"gh.env",
"gitlab.env",
"glab.env",
"codeberg.env",
];
pub(crate) fn load_secret(env_name: &str, secrets_dir: &Path) -> Option<String> {
if let Ok(val) = std::env::var(env_name) {
if !val.is_empty() {
return Some(val);
}
}
if let Err(e) = check_secrets_dir_permissions(secrets_dir) {
eprintln!(
"⚠️ secrets directory permission check failed for {}: {}",
secrets_dir.display(),
e
);
return None;
}
if let Ok(entries) = std::fs::read_dir(secrets_dir) {
let mut secret_paths: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|e| e == "env"))
.collect();
secret_paths.sort_by_key(|path| {
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
let preferred = preferred_secret_file_index(env_name, &file_name);
(preferred, file_name)
});
for path in secret_paths {
#[cfg(unix)]
warn_if_world_readable(&path);
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
if key.trim() == env_name {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
}
}
}
}
None
}
fn preferred_secret_file_index(env_name: &str, file_name: &str) -> usize {
if let Some(index) = PREFERRED_SECRET_FILE_NAMES
.iter()
.position(|preferred| *preferred == file_name)
{
return index;
}
let normalized = env_name.to_ascii_lowercase();
if format!("{normalized}.env") == file_name {
return PREFERRED_SECRET_FILE_NAMES.len();
}
if let Some(stem) = normalized.strip_suffix("_token") {
if format!("{stem}.env") == file_name {
return PREFERRED_SECRET_FILE_NAMES.len() + 1;
}
}
usize::MAX
}
#[cfg(unix)]
fn check_secrets_dir_permissions(dir: &Path) -> Result<(), String> {
if !dir.exists() {
return Ok(());
}
let metadata = std::fs::metadata(dir).map_err(|e| format!("cannot read metadata: {}", e))?;
let mode = metadata.permissions().mode();
if mode & 0o002 != 0 {
return Err(format!(
"directory is world-writable (mode {:o}). Refusing to load secrets.",
mode & 0o7777
));
}
Ok(())
}
#[cfg(not(unix))]
fn check_secrets_dir_permissions(_dir: &Path) -> Result<(), String> {
Ok(())
}
#[cfg(unix)]
fn warn_if_world_readable(path: &Path) {
if let Ok(metadata) = std::fs::metadata(path) {
let mode = metadata.permissions().mode();
if mode & 0o044 != 0 {
eprintln!(
"⚠️ secret file {} is world-readable (mode {:o}). Consider chmod 600.",
path.display(),
mode & 0o7777
);
}
}
}
pub(crate) fn sync_secrets_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".dracon/utilities/sync/secrets")
}