1use serde::{Deserialize, Serialize};
2use std::fmt;
3use uuid::Uuid;
4
5#[derive(Clone, Default, Deserialize, PartialEq)]
11pub struct SensitiveString(String);
12
13impl SensitiveString {
14 pub fn new(s: impl Into<String>) -> Self {
16 Self(s.into())
17 }
18
19 pub fn expose(&self) -> &str {
21 &self.0
22 }
23
24 pub fn is_empty(&self) -> bool {
26 self.0.is_empty()
27 }
28
29 pub fn len(&self) -> usize {
31 self.0.len()
32 }
33}
34
35impl fmt::Debug for SensitiveString {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "***REDACTED***")
38 }
39}
40
41impl fmt::Display for SensitiveString {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "***REDACTED***")
44 }
45}
46
47impl Serialize for SensitiveString {
48 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49 where
50 S: serde::Serializer,
51 {
52 serializer.serialize_str("***REDACTED***")
53 }
54}
55
56impl From<String> for SensitiveString {
57 fn from(s: String) -> Self {
58 Self(s)
59 }
60}
61
62impl From<&str> for SensitiveString {
63 fn from(s: &str) -> Self {
64 Self(s.to_string())
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ExecutionPlan {
71 pub primary: ExecutionTarget,
72 pub fallback_chain: Vec<ExecutionTarget>,
73}
74
75impl ExecutionPlan {
76 pub fn new(primary: ExecutionTarget) -> Self {
77 Self {
78 primary,
79 fallback_chain: Vec::new(),
80 }
81 }
82
83 pub fn with_fallback(mut self, fallback: ExecutionTarget) -> Self {
84 self.fallback_chain.push(fallback);
85 self
86 }
87
88 pub fn with_fallbacks(mut self, fallbacks: Vec<ExecutionTarget>) -> Self {
89 self.fallback_chain.extend(fallbacks);
90 self
91 }
92
93 pub fn all_targets(&self) -> impl Iterator<Item = &ExecutionTarget> {
95 std::iter::once(&self.primary).chain(self.fallback_chain.iter())
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ExecutionTarget {
102 pub provider: String,
103 pub account_id: Uuid,
104 pub endpoint: String,
105 pub upstream_api_key: SensitiveString, }
107
108impl ExecutionTarget {
109 pub fn new(
110 provider: impl Into<String>,
111 account_id: Uuid,
112 endpoint: impl Into<String>,
113 upstream_api_key: impl Into<SensitiveString>,
114 ) -> Self {
115 Self {
116 provider: provider.into(),
117 account_id,
118 endpoint: endpoint.into(),
119 upstream_api_key: upstream_api_key.into(),
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_sensitive_string_creation() {
130 let secret = SensitiveString::new("my-secret-key");
131 assert_eq!(secret.expose(), "my-secret-key");
132 assert_eq!(secret.len(), 13);
133 assert!(!secret.is_empty());
134 }
135
136 #[test]
137 fn test_sensitive_string_debug() {
138 let secret = SensitiveString::new("my-secret-key");
139 let debug_str = format!("{:?}", secret);
140 assert_eq!(debug_str, "***REDACTED***");
141 assert!(!debug_str.contains("my-secret-key"));
142 }
143
144 #[test]
145 fn test_sensitive_string_display() {
146 let secret = SensitiveString::new("my-secret-key");
147 let display_str = format!("{}", secret);
148 assert_eq!(display_str, "***REDACTED***");
149 assert!(!display_str.contains("my-secret-key"));
150 }
151
152 #[test]
153 fn test_sensitive_string_serialize() {
154 let secret = SensitiveString::new("my-secret-key");
155 let json = serde_json::to_string(&secret).unwrap();
156 assert_eq!(json, "\"***REDACTED***\"");
157 assert!(!json.contains("my-secret-key"));
158 }
159
160 #[test]
161 fn test_sensitive_string_deserialize() {
162 let json = "\"my-secret-key\"";
163 let secret: SensitiveString = serde_json::from_str(json).unwrap();
164 assert_eq!(secret.expose(), "my-secret-key");
165 }
166
167 #[test]
168 fn test_sensitive_string_default() {
169 let secret = SensitiveString::default();
170 assert!(secret.is_empty());
171 assert_eq!(secret.len(), 0);
172 }
173
174 #[test]
175 fn test_sensitive_string_partial_eq() {
176 let s1 = SensitiveString::new("key");
177 let s2 = SensitiveString::new("key");
178 let s3 = SensitiveString::new("different");
179 assert_eq!(s1, s2);
180 assert_ne!(s1, s3);
181 }
182
183 #[test]
184 fn test_sensitive_string_partial_eq_no_leak() {
185 let s1 = SensitiveString::new("secret-key-123");
187 let s2 = SensitiveString::new("secret-key-123");
188 let s3 = SensitiveString::new("different-key");
189
190 assert!(s1 == s2);
192 assert!(s1 != s3);
193
194 let debug = format!("{:?}", s1);
196 assert!(!debug.contains("secret-key-123"));
197
198 let display = format!("{}", s1);
200 assert!(!display.contains("secret-key-123"));
201
202 assert_eq!(s1.expose(), "secret-key-123");
204 }
205
206 #[test]
207 fn test_sensitive_string_from_str() {
208 let s1: SensitiveString = "test-key".into();
209 let s2 = SensitiveString::new("test-key");
210 assert_eq!(s1, s2);
211 assert_eq!(s1.expose(), "test-key");
212 }
213
214 #[test]
215 fn test_sensitive_string_from_string() {
216 let s1: SensitiveString = String::from("test-key").into();
217 let s2 = SensitiveString::new("test-key");
218 assert_eq!(s1, s2);
219 }
220
221 #[test]
222 fn test_execution_plan_new() {
223 let target = ExecutionTarget::new(
224 "openai",
225 Uuid::new_v4(),
226 "https://api.openai.com",
227 "sk-test-key",
228 );
229 let plan = ExecutionPlan::new(target);
230 assert_eq!(plan.primary.provider, "openai");
231 assert!(plan.fallback_chain.is_empty());
232 }
233
234 #[test]
235 fn test_execution_plan_with_fallback() {
236 let primary = ExecutionTarget::new(
237 "openai",
238 Uuid::new_v4(),
239 "https://api.openai.com",
240 "sk-primary-key",
241 );
242 let fallback = ExecutionTarget::new(
243 "claude",
244 Uuid::new_v4(),
245 "https://api.anthropic.com",
246 "sk-fallback-key",
247 );
248 let plan = ExecutionPlan::new(primary).with_fallback(fallback);
249 assert_eq!(plan.fallback_chain.len(), 1);
250 assert_eq!(plan.fallback_chain[0].provider, "claude");
251 }
252
253 #[test]
254 fn test_execution_plan_all_targets() {
255 let primary = ExecutionTarget::new(
256 "openai",
257 Uuid::new_v4(),
258 "https://api.openai.com",
259 "sk-primary-key",
260 );
261 let fallback1 = ExecutionTarget::new(
262 "claude",
263 Uuid::new_v4(),
264 "https://api.anthropic.com",
265 "sk-fallback1-key",
266 );
267 let fallback2 = ExecutionTarget::new(
268 "gemini",
269 Uuid::new_v4(),
270 "https://api.gemini.com",
271 "sk-fallback2-key",
272 );
273 let plan = ExecutionPlan::new(primary)
274 .with_fallback(fallback1)
275 .with_fallback(fallback2);
276
277 let targets: Vec<_> = plan.all_targets().collect();
278 assert_eq!(targets.len(), 3);
279 assert_eq!(targets[0].provider, "openai");
280 assert_eq!(targets[1].provider, "claude");
281 assert_eq!(targets[2].provider, "gemini");
282 }
283
284 #[test]
285 fn test_execution_target_api_key_hidden_in_debug() {
286 let target = ExecutionTarget::new(
287 "openai",
288 Uuid::new_v4(),
289 "https://api.openai.com",
290 "sk-secret-key",
291 );
292 let debug_str = format!("{:?}", target);
293 assert!(debug_str.contains("***REDACTED***"));
294 assert!(!debug_str.contains("sk-secret-key"));
295 }
296
297 #[test]
298 fn test_execution_target_api_key_hidden_in_serialize() {
299 let target = ExecutionTarget::new(
300 "openai",
301 Uuid::new_v4(),
302 "https://api.openai.com",
303 "sk-secret-key",
304 );
305 let json = serde_json::to_string(&target).unwrap();
306 assert!(json.contains("***REDACTED***"));
307 assert!(!json.contains("sk-secret-key"));
308 }
309
310 #[test]
311 fn test_execution_target_api_key_expose() {
312 let target = ExecutionTarget::new(
313 "openai",
314 Uuid::new_v4(),
315 "https://api.openai.com",
316 "sk-secret-key",
317 );
318 assert_eq!(target.upstream_api_key.expose(), "sk-secret-key");
320 }
321}