1//! The credentials Whiskers' outbound adapters need, by name. 2//! 3//! A name here is the one an operator sets it under on **every** backend: an environment variable 4//! natively, a Worker secret on Cloudflare. Hosting differs in where the value is kept and how 5//! protected it is (celld, for one, has no secret store and keeps `vars` in the fleet's bucket); that 6//! is an adapter's property to state, not a different name to learn. The set is closed, so a key that 7//! is not listed here cannot be asked for. 8//! 9//! "Not set" and "set to nothing" are different facts (an operator who put an empty value in has 10//! made a mistake worth saying so, where an absent one is just not configured), so a lookup says which. 11 12use std::fmt; 13 14use crate::error::StoreError; 15 16/// A credential Whiskers can be given. 17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 18pub enum SecretName { 19 /// The key for Jev (the guard and the reranker). 20 JevKey, 21 /// The key for the natural voice. 22 VoiceKey, 23 /// Which voice speaks. Not a credential in itself, but it is handled like one: it is never logged. 24 VoiceId, 25} 26 27impl SecretName { 28 pub const ALL: [SecretName; 3] = [SecretName::JevKey, SecretName::VoiceKey, SecretName::VoiceId]; 29 30 /// The name an operator sets it under. 31 pub const fn operator_name(self) -> &'static str { 32 match self { 33 SecretName::JevKey => "TYPESAFE_API_KEY", 34 SecretName::VoiceKey => "ELEVENLABS_API_KEY", 35 SecretName::VoiceId => "ELEVENLABS_VOICE_ID", 36 } 37 } 38} 39 40impl fmt::Display for SecretName { 41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 f.write_str(self.operator_name()) 43 } 44} 45 46/// A secret's value. Its `Debug` and `Display` say nothing, so it cannot reach a log by accident; 47/// the only way to the text is [`expose`](Self::expose), which is greppable. 48#[derive(Clone, PartialEq, Eq)] 49pub struct SecretValue(String); 50 51impl SecretValue { 52 pub fn new(value: impl Into<String>) -> Self { 53 Self(value.into()) 54 } 55 56 pub fn expose(&self) -> &str { 57 &self.0 58 } 59} 60 61impl fmt::Debug for SecretValue { 62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 write!(f, "SecretValue(..{} bytes..)", self.0.len()) 64 } 65} 66 67/// What a lookup found. 68#[derive(Clone, Debug, PartialEq, Eq)] 69pub enum Lookup { 70 /// Nothing is set under that name. 71 Absent, 72 /// Something is set and it is empty (or only whitespace). 73 Empty, 74 /// A value, with surrounding whitespace removed (a pasted key often brings a newline). 75 Present(SecretValue), 76} 77 78impl Lookup { 79 /// The value, if there is one. Callers that treat "empty" like "absent" say so by calling this. 80 pub fn present(self) -> Option<SecretValue> { 81 match self { 82 Lookup::Present(v) => Some(v), 83 Lookup::Absent | Lookup::Empty => None, 84 } 85 } 86 87 /// Classifies raw text from a store: the one place the rule is written. 88 pub fn of(raw: Option<&str>) -> Self { 89 match raw.map(str::trim) { 90 None => Lookup::Absent, 91 Some("") => Lookup::Empty, 92 Some(v) => Lookup::Present(SecretValue::new(v)), 93 } 94 } 95} 96 97/// Where credentials come from. 98/// 99/// Contract: a name that was never set is `Absent`, one set to nothing is `Empty`, anything else is 100/// `Present` and trimmed. A lookup changes nothing. `Err` means the store itself could not be asked, 101/// which is not the same as the secret being absent. 102#[expect(async_fn_in_trait, reason = "a secret store may be a binding that is awaited, and a Worker's futures cannot be Send")] 103pub trait Secrets { 104 async fn get(&self, name: SecretName) -> Result<Lookup, StoreError>; 105} 106 107#[cfg(test)] 108mod tests { 109 use super::*; 110 111 #[test] 112 fn absent_empty_and_present_are_three_different_things() { 113 assert_eq!(Lookup::of(None), Lookup::Absent); 114 assert_eq!(Lookup::of(Some("")), Lookup::Empty); 115 assert_eq!(Lookup::of(Some(" \n")), Lookup::Empty); 116 assert_eq!(Lookup::of(Some(" k\n")), Lookup::Present(SecretValue::new("k"))); 117 assert!(Lookup::Empty.present().is_none() && Lookup::Absent.present().is_none()); 118 } 119 120 #[test] 121 fn a_secret_never_prints_itself() { 122 let s = SecretValue::new("hunter2"); 123 assert!(!format!("{s:?}").contains("hunter2")); 124 } 125 126 #[test] 127 fn the_operator_names_are_the_documented_ones() { 128 assert_eq!(SecretName::JevKey.to_string(), "TYPESAFE_API_KEY"); 129 assert_eq!(SecretName::VoiceKey.to_string(), "ELEVENLABS_API_KEY"); 130 assert_eq!(SecretName::VoiceId.to_string(), "ELEVENLABS_VOICE_ID"); 131 } 132}