use parking_lot::RwLock;
use std::collections::HashMap;
#[derive(Clone)]
pub struct Secret(String);
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Secret(***)")
}
}
pub trait CredentialStore: Send + Sync {
fn resolve(&self, id: &str) -> Option<Secret>;
}
#[derive(Default)]
pub struct InMemoryCredentials {
secrets: RwLock<HashMap<String, Secret>>,
}
impl InMemoryCredentials {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, id: impl Into<String>, secret: Secret) {
self.secrets.write().insert(id.into(), secret);
}
}
impl CredentialStore for InMemoryCredentials {
fn resolve(&self, id: &str) -> Option<Secret> {
self.secrets.read().get(id).cloned()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn secret_debug_is_redacted() {
let s = Secret::new("ghp_supersecret");
assert_eq!(format!("{s:?}"), "Secret(***)");
assert_eq!(s.expose(), "ghp_supersecret");
}
#[test]
fn store_resolves_known_and_misses_unknown() {
let store = InMemoryCredentials::new();
store.insert("github-app", Secret::new("token-123"));
assert_eq!(store.resolve("github-app").unwrap().expose(), "token-123");
assert!(store.resolve("nope").is_none());
}
}