feat(notsecrets): add multi-provider secret resolution system
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled

Add a config-driven, strongly-typed secret/env resolution system to
notsecrets with 11 provider backends. The system supports explicit
key bindings with typed SecretRef enums and a priority chain fallback.

Tier 1 providers (implemented): Env, 1Password, dotenvx, SOPS, GSM
Tier 2 providers (stubbed): nuenv, direnv, mise, Bitwarden, Vault, dotenvy

New types: Provider, ProviderConfig, SecretRef, SecretsConfig,
SecretsError, SecretSource, EnumerableSecretSource, SecretResolver

Config lives in notsecrets.toml with typed serde deserialization.
ISP split: EnumerableSecretSource for backends that support bulk
enumeration, SecretSource for single-key lookup only.

Includes conformance test suite, 33 new tests (161 workspace total).
notstrap EnvInjector migration deferred to follow-up.
This commit is contained in:
2026-06-06 09:49:05 -04:00
parent 65d1a2dc2e
commit 1359def673
27 changed files with 3079 additions and 39 deletions

View File

@@ -0,0 +1,92 @@
use crate::config::{Provider, SecretRef};
use crate::error::SecretsError;
use crate::ports::SecretSource;
use std::process::Command;
pub struct GsmSource {
project: String,
}
impl GsmSource {
pub fn new(project: String) -> Self {
Self { project }
}
}
impl SecretSource for GsmSource {
fn name(&self) -> &str {
"gsm"
}
fn provider(&self) -> Provider {
Provider::Gsm
}
fn resolve(&self, _key: &str) -> Result<Option<String>, SecretsError> {
Ok(None)
}
fn resolve_ref(
&self,
key: &str,
secret_ref: &SecretRef,
) -> Result<Option<String>, SecretsError> {
let SecretRef::Gsm { path } = secret_ref else {
return self.resolve(key);
};
let output = Command::new("gcloud")
.args([
"secrets",
"versions",
"access",
"latest",
"--secret",
path,
"--project",
&self.project,
])
.output()
.map_err(|e| SecretsError::SourceError {
name: "gsm".to_string(),
source: e.into(),
})?;
if output.status.success() {
Ok(Some(
String::from_utf8_lossy(&output.stdout).trim().to_string(),
))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(SecretsError::SourceError {
name: "gsm".to_string(),
source: anyhow::anyhow!("gcloud secrets failed: {stderr}"),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gsm_source_name_and_provider() {
let source = GsmSource::new("my-project".to_string());
assert_eq!(source.name(), "gsm");
assert_eq!(source.provider(), Provider::Gsm);
}
#[test]
fn gsm_source_resolve_without_ref_returns_none() {
let source = GsmSource::new("my-project".to_string());
let result = source.resolve("KEY").unwrap();
assert_eq!(result, None);
}
#[test]
fn gsm_source_resolve_ref_wrong_variant_delegates() {
let source = GsmSource::new("my-project".to_string());
let wrong_ref = SecretRef::Env;
let result = source.resolve_ref("KEY", &wrong_ref).unwrap();
assert_eq!(result, None);
}
}