use std::collections::{
HashMap,
HashSet,
};
use std::io::Write;
use std::path::{
Path,
PathBuf,
};
use crate::error::{
SecretsError,
SecretsResult,
};
use crate::manager::SecretManager;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Arch {
Amd64,
Arm64,
Arm,
I386,
Riscv64,
Ppc64le,
S390x,
}
impl Arch {
pub fn as_str(&self) -> &'static str {
match self {
Self::Amd64 => "amd64",
Self::Arm64 => "arm64",
Self::Arm => "arm",
Self::I386 => "i386",
Self::Riscv64 => "riscv64",
Self::Ppc64le => "ppc64le",
Self::S390x => "s390x",
}
}
}
impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Arch {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s_lower = s.to_lowercase();
match s_lower.as_str() {
"amd64" | "x64" | "x86_64" => Ok(Self::Amd64),
"arm64" | "aarch64" => Ok(Self::Arm64),
"arm" | "armv7" | "armv7l" | "armhf" => Ok(Self::Arm),
"i386" | "i686" | "x86" => Ok(Self::I386),
"riscv64" | "riscv64gc" => Ok(Self::Riscv64),
"ppc64le" | "powerpc64le" => Ok(Self::Ppc64le),
"s390x" => Ok(Self::S390x),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Os {
Linux,
Macos,
Windows,
Freebsd,
Openbsd,
Netbsd,
Android,
Ios,
}
impl Os {
pub fn as_str(&self) -> &'static str {
match self {
Self::Linux => "linux",
Self::Macos => "macos",
Self::Windows => "windows",
Self::Freebsd => "freebsd",
Self::Openbsd => "openbsd",
Self::Netbsd => "netbsd",
Self::Android => "android",
Self::Ios => "ios",
}
}
}
impl std::fmt::Display for Os {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Os {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s_lower = s.to_lowercase();
match s_lower.as_str() {
"linux" => Ok(Self::Linux),
"macos" | "darwin" | "osx" => Ok(Self::Macos),
"windows" | "win32" | "win" => Ok(Self::Windows),
"freebsd" => Ok(Self::Freebsd),
"openbsd" => Ok(Self::Openbsd),
"netbsd" => Ok(Self::Netbsd),
"android" => Ok(Self::Android),
"ios" => Ok(Self::Ios),
_ => Err(()),
}
}
}
pub struct EnvLoader {
manager: SecretManager,
}
impl EnvLoader {
fn find_file_case_insensitive(dir: &Path, filename: &str) -> Option<PathBuf> {
let target = filename.to_lowercase();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().to_lowercase() == target {
return Some(entry.path());
}
}
}
None
}
fn add_exact_if_exist(dir: &Path, paths: &mut Vec<PathBuf>, filename: &str) {
if let Some(p) = Self::find_file_case_insensitive(dir, filename)
&& !paths.iter().any(|x| x == &p)
{
paths.push(p);
}
}
pub fn new() -> SecretsResult<Self> {
Ok(Self {
manager: SecretManager::new()?,
})
}
pub fn with_manager(manager: SecretManager) -> Self {
Self { manager }
}
pub fn load(&self) -> SecretsResult<Vec<PathBuf>> {
self.load_from_dir(".")
}
pub fn load_from_dir(&self, dir: impl AsRef<Path>) -> SecretsResult<Vec<PathBuf>> {
let dir = dir.as_ref();
let mut env_vars = HashMap::new();
let mut loaded_files: HashSet<PathBuf> = HashSet::new();
let mut loaded_order: Vec<PathBuf> = Vec::new();
if let Some(base_path) = Self::find_file_case_insensitive(dir, ".env")
&& base_path.exists()
{
let vars = self.load_env_file(&base_path)?;
env_vars.extend(vars.clone());
loaded_order.push(base_path.clone());
loaded_files.insert(base_path);
for (key, value) in Self::discover_dimensions_from_vars(&vars) {
unsafe {
std::env::set_var(&key, &value);
}
}
}
loop {
let candidate_paths = self.resolve_env_paths(dir);
let next_file = candidate_paths
.into_iter()
.find(|p| !loaded_files.contains(p) && p.exists());
let Some(path) = next_file else {
break;
};
let vars = self.load_env_file(&path)?;
env_vars.extend(vars.clone());
loaded_order.push(path.clone());
loaded_files.insert(path);
for (key, value) in Self::discover_dimensions_from_vars(&vars) {
unsafe {
std::env::set_var(&key, &value);
}
}
}
for (k, v) in env_vars {
unsafe {
std::env::set_var(k, v);
}
}
Ok(loaded_order)
}
pub fn collect_all_vars_from_dir(
&self,
dir: impl AsRef<Path>,
) -> SecretsResult<(HashMap<String, String>, Vec<PathBuf>)> {
let dir = dir.as_ref();
let mut env_vars = HashMap::new();
let mut loaded_files: HashSet<PathBuf> = HashSet::new();
let mut loaded_order: Vec<PathBuf> = Vec::new();
let saved_env = Self::save_dimension_env_vars();
if let Some(base_path) = Self::find_file_case_insensitive(dir, ".env")
&& base_path.exists()
{
let vars = self.load_env_file(&base_path)?;
env_vars.extend(vars.clone());
loaded_order.push(base_path.clone());
loaded_files.insert(base_path);
for (key, value) in Self::discover_dimensions_from_vars(&vars) {
unsafe {
std::env::set_var(&key, &value);
}
}
}
loop {
let candidate_paths = self.resolve_env_paths(dir);
let next_file = candidate_paths
.into_iter()
.find(|p| !loaded_files.contains(p) && p.exists());
let Some(path) = next_file else {
break;
};
let vars = self.load_env_file(&path)?;
env_vars.extend(vars.clone());
loaded_order.push(path.clone());
loaded_files.insert(path);
for (key, value) in Self::discover_dimensions_from_vars(&vars) {
unsafe {
std::env::set_var(&key, &value);
}
}
}
Self::restore_dimension_env_vars(&saved_env);
Ok((env_vars, loaded_order))
}
fn save_dimension_env_vars() -> HashMap<String, Option<String>> {
let keys = [
"DOTENVAGE_ENV",
"EKG_ENV",
"VERCEL_ENV",
"NODE_ENV",
"DOTENVAGE_OS",
"EKG_OS",
"DOTENVAGE_ARCH",
"EKG_ARCH",
"DOTENVAGE_USER",
"EKG_USER",
"DOTENVAGE_VARIANT",
"EKG_VARIANT",
"VARIANT",
];
keys.iter()
.map(|&k| (k.to_string(), std::env::var(k).ok()))
.collect()
}
fn restore_dimension_env_vars(saved: &HashMap<String, Option<String>>) {
for (key, value) in saved {
unsafe {
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
}
pub fn resolve_env_paths(&self, dir: &Path) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = Vec::new();
Self::add_exact_if_exist(dir, &mut paths, ".env");
Self::discover_env_from_env_file(dir);
let env = Self::resolve_env();
let os = Self::resolve_os();
let arch = Self::resolve_arch();
let user = Self::resolve_user();
let variant = Self::resolve_variant();
for mask in 1..32 {
let mut parts = Vec::new();
if mask & 1 != 0 {
parts.push(env.as_str());
}
if mask & 2 != 0 {
if let Some(ref o) = os {
parts.push(o.as_str());
} else {
continue; }
}
if mask & 4 != 0 {
if let Some(ref a) = arch {
parts.push(a.as_str());
} else {
continue; }
}
if mask & 8 != 0 {
if let Some(ref u) = user {
parts.push(u.as_str());
} else {
continue; }
}
if mask & 16 != 0 {
if let Some(ref v) = variant {
parts.push(v.as_str());
} else {
continue; }
}
let filename = format!(".env.{}", parts.join("."));
Self::add_exact_if_exist(dir, &mut paths, &filename);
}
if let Some(pr_number) = Self::resolve_pr_number() {
Self::add_exact_if_exist(dir, &mut paths, &format!(".env.pr-{}", pr_number));
}
paths
}
pub fn load_env_file(&self, path: &Path) -> SecretsResult<HashMap<String, String>> {
let content =
std::fs::read_to_string(path).map_err(|e| SecretsError::EnvFileReadFailed {
path: path.display().to_string(),
reason: e.to_string(),
})?;
self.parse_and_decrypt(&content, path)
}
pub fn parse_and_decrypt(
&self,
content: &str,
path: &Path,
) -> SecretsResult<HashMap<String, String>> {
let mut vars = HashMap::new();
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_string();
let mut value = value.trim().to_string();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
value = value[1..value.len() - 1].to_string();
}
let decrypted = self.manager.decrypt_value(&value).map_err(|e| {
SecretsError::EnvFileParseFailed {
path: path.display().to_string(),
reason: format!("line {} for '{}': {}", line_num + 1, key, e),
}
})?;
vars.insert(key, decrypted);
}
}
Ok(vars)
}
pub fn get_var(&self, key: &str) -> SecretsResult<String> {
let value = std::env::var(key).map_err(|_| SecretsError::EnvVarNotFound {
key: key.to_string(),
})?;
self.manager.decrypt_value(&value)
}
pub fn get_var_or(&self, key: &str, default: &str) -> String {
self.get_var(key).unwrap_or_else(|_| default.to_string())
}
pub fn set_var(&self, key: &str, value: &str) -> SecretsResult<PathBuf> {
self.set_var_in_dir(key, value, ".")
}
pub fn set_var_in_dir(
&self,
key: &str,
value: &str,
dir: impl AsRef<Path>,
) -> SecretsResult<PathBuf> {
let path = dir.as_ref().join(".env.local");
self.set_var_in_file(key, value, &path)?;
Ok(path)
}
pub fn set_var_in_file(
&self,
key: &str,
value: &str,
path: impl AsRef<Path>,
) -> SecretsResult<()> {
let path = path.as_ref();
let mut vars = Self::read_env_file_for_write(path)?;
let final_value =
if !Self::is_age_key_variable(key) && AutoDetectPatterns::should_encrypt(key) {
self.manager.encrypt_value(value)?
} else {
value.to_string()
};
vars.insert(key.to_string(), final_value);
Self::write_env_file_for_write(path, &vars)
}
pub fn unset_var(&self, key: &str) -> SecretsResult<PathBuf> {
self.unset_var_in_dir(key, ".")
}
pub fn unset_var_in_dir(&self, key: &str, dir: impl AsRef<Path>) -> SecretsResult<PathBuf> {
let path = dir.as_ref().join(".env.local");
self.unset_var_in_file(key, &path)?;
Ok(path)
}
pub fn unset_var_in_file(&self, key: &str, path: impl AsRef<Path>) -> SecretsResult<()> {
let path = path.as_ref();
let mut vars = Self::read_env_file_for_write(path)?;
if vars.remove(key).is_some() {
Self::write_env_file_for_write(path, &vars)?;
}
Ok(())
}
fn read_env_file_for_write(path: &Path) -> SecretsResult<HashMap<String, String>> {
if !path.exists() {
return Ok(HashMap::new());
}
let content =
std::fs::read_to_string(path).map_err(|e| SecretsError::EnvFileReadFailed {
path: path.display().to_string(),
reason: e.to_string(),
})?;
dotenvy::from_read_iter(content.as_bytes())
.collect::<Result<HashMap<String, String>, _>>()
.map_err(|e| SecretsError::EnvFileParseFailed {
path: path.display().to_string(),
reason: e.to_string(),
})
}
fn write_env_file_for_write(path: &Path, vars: &HashMap<String, String>) -> SecretsResult<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
}
let mut file =
std::fs::File::create(path).map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
let mut keys: Vec<_> = vars.keys().cloned().collect();
keys.sort();
for key in keys {
let value = vars
.get(&key)
.ok_or_else(|| SecretsError::WriteFailed("missing key during write".to_string()))?;
if Self::needs_env_file_quoting(value) {
writeln!(file, "{}=\"{}\"", key, Self::escape_env_file_value(value))
.map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
} else {
writeln!(file, "{}={}", key, value)
.map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
}
}
Ok(())
}
fn needs_env_file_quoting(value: &str) -> bool {
value.is_empty()
|| value.contains(char::is_whitespace)
|| value.contains('$')
|| value.contains('\n')
|| value.contains('"')
}
fn escape_env_file_value(value: &str) -> String {
value.replace('"', "\\\"")
}
pub fn get_all_variable_names(&self) -> SecretsResult<Vec<String>> {
self.get_all_variable_names_from_dir(".")
}
pub fn get_all_variable_names_from_dir(
&self,
dir: impl AsRef<Path>,
) -> SecretsResult<Vec<String>> {
let (vars, _paths) = self.collect_all_vars_from_dir(dir)?;
Ok(vars
.keys()
.filter(|k| !Self::is_age_key_variable(k))
.cloned()
.collect())
}
pub fn dump_to_writer<W: std::io::Write>(&self, writer: W) -> SecretsResult<()> {
self.dump_to_writer_from_dir(".", writer)
}
pub fn dump_to_writer_from_dir<W: std::io::Write>(
&self,
dir: impl AsRef<Path>,
mut writer: W,
) -> SecretsResult<()> {
let (merged_vars, _paths) = self.collect_all_vars_from_dir(dir)?;
let mut keys: Vec<_> = merged_vars.keys().cloned().collect();
keys.sort();
for key in keys {
if Self::is_age_key_variable(&key) {
continue;
}
if let Some(value) = merged_vars.get(&key) {
let decrypted_value = self.manager.decrypt_value(value)?;
if Self::needs_quoting(&decrypted_value) {
writeln!(
writer,
"{}=\"{}\"",
key,
Self::escape_value(&decrypted_value)
)
.map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
} else {
writeln!(writer, "{}={}", key, decrypted_value)
.map_err(|e| SecretsError::WriteFailed(e.to_string()))?;
}
}
}
Ok(())
}
fn is_age_key_variable(key: &str) -> bool {
let key_upper = key.to_uppercase();
matches!(
key_upper.as_str(),
"DOTENVAGE_AGE_KEY" | "AGE_KEY" | "EKG_AGE_KEY" | "AGE_KEY_NAME"
) || key_upper.ends_with("_AGE_KEY_NAME")
}
fn needs_quoting(value: &str) -> bool {
value.is_empty()
|| value.contains(char::is_whitespace)
|| value.contains('"')
|| value.contains('\'')
|| value.contains('=')
|| value.contains('#')
}
fn escape_value(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn discover_env_from_env_file(dir: &Path) {
if std::env::var("DOTENVAGE_ENV").is_ok()
|| std::env::var("EKG_ENV").is_ok()
|| std::env::var("VERCEL_ENV").is_ok()
|| std::env::var("NODE_ENV").is_ok()
{
return;
}
let env_file = dir.join(".env");
if let Some(config_key) = Self::find_env_config_in_file(&env_file) {
unsafe {
std::env::set_var(config_key.0, config_key.1);
}
}
}
fn find_env_config_in_file(file_path: &Path) -> Option<(String, String)> {
let content = std::fs::read_to_string(file_path).ok()?;
for key in &["DOTENVAGE_ENV", "EKG_ENV", "VERCEL_ENV", "NODE_ENV"] {
if let Some(value) = Self::find_key_value_in_content(&content, key) {
return Some((key.to_string(), value));
}
}
None
}
fn find_key_value_in_content(content: &str, key: &str) -> Option<String> {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (line_key, value) = line.split_once('=')?;
let line_key = line_key.trim();
let value = value.trim().trim_matches('"').trim_matches('\'');
if line_key != key || value.is_empty() {
continue;
}
if SecretManager::is_encrypted(value) {
continue;
}
return Some(value.to_string());
}
None
}
fn discover_dimensions_from_vars(vars: &HashMap<String, String>) -> Vec<(String, String)> {
let mut discovered = Vec::new();
let env_keys = ["DOTENVAGE_ENV", "EKG_ENV", "VERCEL_ENV", "NODE_ENV"];
if !env_keys
.iter()
.any(|k| std::env::var(k).ok().filter(|s| !s.is_empty()).is_some())
{
for key in &env_keys {
if let Some(value) = vars.get(*key)
&& !value.is_empty()
&& !SecretManager::is_encrypted(value)
{
discovered.push((key.to_string(), value.clone()));
break;
}
}
}
let os_keys = ["DOTENVAGE_OS", "EKG_OS"];
if !os_keys
.iter()
.any(|k| std::env::var(k).ok().filter(|s| !s.is_empty()).is_some())
{
for key in &os_keys {
if let Some(value) = vars.get(*key)
&& !value.is_empty()
&& !SecretManager::is_encrypted(value)
{
discovered.push((key.to_string(), value.clone()));
break;
}
}
}
let arch_keys = ["DOTENVAGE_ARCH", "EKG_ARCH"];
if !arch_keys
.iter()
.any(|k| std::env::var(k).ok().filter(|s| !s.is_empty()).is_some())
{
for key in &arch_keys {
if let Some(value) = vars.get(*key)
&& !value.is_empty()
&& !SecretManager::is_encrypted(value)
{
discovered.push((key.to_string(), value.clone()));
break;
}
}
}
let user_keys = ["DOTENVAGE_USER", "EKG_USER"];
if !user_keys
.iter()
.any(|k| std::env::var(k).ok().filter(|s| !s.is_empty()).is_some())
{
for key in &user_keys {
if let Some(value) = vars.get(*key)
&& !value.is_empty()
&& !SecretManager::is_encrypted(value)
{
discovered.push((key.to_string(), value.clone()));
break;
}
}
}
let variant_keys = ["DOTENVAGE_VARIANT", "EKG_VARIANT", "VARIANT"];
if !variant_keys
.iter()
.any(|k| std::env::var(k).ok().filter(|s| !s.is_empty()).is_some())
{
for key in &variant_keys {
if let Some(value) = vars.get(*key)
&& !value.is_empty()
&& !SecretManager::is_encrypted(value)
{
discovered.push((key.to_string(), value.clone()));
break;
}
}
}
discovered
}
pub fn resolve_env() -> String {
std::env::var("DOTENVAGE_ENV")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("EKG_ENV").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("VERCEL_ENV").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("NODE_ENV").ok().filter(|s| !s.is_empty()))
.map(|e| e.to_lowercase())
.unwrap_or_else(|| "local".to_string())
}
pub fn resolve_arch() -> Option<String> {
let arch = std::env::var("DOTENVAGE_ARCH")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("EKG_ARCH").ok().filter(|s| !s.is_empty()))
.or_else(|| {
std::env::var("CARGO_CFG_TARGET_ARCH")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(|| {
std::env::var("TARGET")
.ok()
.filter(|s| !s.is_empty())
.and_then(|t| t.split('-').next().map(String::from))
})
.or_else(|| std::env::var("TARGETARCH").ok().filter(|s| !s.is_empty()))
.or_else(|| {
std::env::var("TARGETPLATFORM")
.ok()
.filter(|s| !s.is_empty())
.and_then(|p| p.split('/').nth(1).map(String::from))
})
.or_else(|| std::env::var("RUNNER_ARCH").ok().filter(|s| !s.is_empty()))?;
Some(
arch.parse::<Arch>()
.map(|a| a.to_string())
.unwrap_or_else(|_| arch.to_lowercase()),
)
}
pub fn resolve_os() -> Option<String> {
let os = std::env::var("DOTENVAGE_OS")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("EKG_OS").ok().filter(|s| !s.is_empty()))
.or_else(|| {
std::env::var("CARGO_CFG_TARGET_OS")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(|| {
std::env::var("TARGET")
.ok()
.filter(|s| !s.is_empty())
.and_then(|t| t.split('-').nth(2).map(String::from))
})
.or_else(|| std::env::var("RUNNER_OS").ok().filter(|s| !s.is_empty()))
.or_else(|| Some(std::env::consts::OS.to_string()))?;
Some(
os.parse::<Os>()
.map(|o| o.to_string())
.unwrap_or_else(|_| os.to_lowercase()),
)
}
pub fn resolve_user() -> Option<String> {
std::env::var("DOTENVAGE_USER")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("EKG_USER").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("GITHUB_ACTOR").ok().filter(|s| !s.is_empty()))
.or_else(|| {
std::env::var("GITHUB_TRIGGERING_ACTOR")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(|| {
std::env::var("GITHUB_REPOSITORY_OWNER")
.ok()
.filter(|s| !s.is_empty())
})
.or_else(|| std::env::var("USER").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("USERNAME").ok().filter(|s| !s.is_empty()))
.map(|u| u.to_lowercase())
}
pub fn resolve_variant() -> Option<String> {
std::env::var("DOTENVAGE_VARIANT")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("EKG_VARIANT").ok().filter(|s| !s.is_empty()))
.or_else(|| std::env::var("VARIANT").ok().filter(|s| !s.is_empty()))
.map(|v| v.to_lowercase())
}
pub fn resolve_pr_number() -> Option<String> {
if let Ok(event) = std::env::var("GITHUB_EVENT_NAME")
&& event.starts_with("pull_request")
&& let Some(pr) = std::env::var("PR_NUMBER").ok().filter(|s| !s.is_empty())
{
return Some(pr);
}
if let Ok(gref) = std::env::var("GITHUB_REF")
&& let Some(idx) = gref.find("/pull/")
{
let mut pr_number = String::new();
for c in gref[idx + 6..].chars() {
if c.is_ascii_digit() {
pr_number.push(c);
} else {
break;
}
}
if !pr_number.is_empty() {
return Some(pr_number);
}
}
None
}
}
pub struct AutoDetectPatterns;
impl AutoDetectPatterns {
pub const ENCRYPT_PATTERNS: &'static [&'static str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSPHRASE",
"PHRASE",
"CREDENTIAL",
"_KEY",
"API_KEY",
"PRIVATE_KEY",
];
pub const NEVER_ENCRYPT: &'static [&'static str] = &[
"AWS_REGION",
"FLY_PRIMARY_REGION",
"PORT",
"RUST_LOG",
"DATABASE_NAME",
"APP_NAME",
"ENDPOINT_URL",
"ORG",
"PUBLIC_KEY",
"PUB_KEY",
];
pub fn should_encrypt(key: &str) -> bool {
if Self::is_age_key_variable(key) {
return false;
}
let key_upper = key.to_uppercase();
if Self::NEVER_ENCRYPT.iter().any(|p| key_upper.contains(p)) {
return false;
}
Self::ENCRYPT_PATTERNS.iter().any(|p| key_upper.contains(p))
}
pub fn is_age_key_variable(key: &str) -> bool {
let key_upper = key.to_uppercase();
matches!(
key_upper.as_str(),
"DOTENVAGE_AGE_KEY" | "AGE_KEY" | "EKG_AGE_KEY" | "AGE_KEY_NAME"
) || key_upper.ends_with("_AGE_KEY_NAME")
}
}