| 1 | //! Injectable ambient-environment access for credential resolution. |
| 2 | //! |
| 3 | //! Ported from pi-mono `packages/ai/src/auth/context.ts` and the `AuthContext` |
| 4 | //! interface in `packages/ai/src/auth/types.ts` (MIT, Copyright (c) 2025 Mario |
| 5 | //! Zechner — full notice in the parent module). |
| 6 | //! |
| 7 | //! pi's motivation applies here unchanged: resolution that reads |
| 8 | //! `process.env` / `std::env` directly is untestable without mutating the real |
| 9 | //! process. Every ambient read the resolver performs itself goes through this |
| 10 | //! trait, so a test can state exactly which variables exist. Resolution never |
| 11 | //! stats the filesystem (#5772): external credential discovery is |
| 12 | //! metadata-only, so the trait deliberately has no file probe. |
| 13 | |
| 14 | #[cfg(test)] |
| 15 | use std::collections::BTreeMap; |
| 16 | |
| 17 | /// Ambient environment access for credential resolution. |
| 18 | pub(crate) trait AuthContext: Send + Sync { |
| 19 | /// Read an environment variable, treating blank values as unset — a blank |
| 20 | /// `DEEPSEEK_API_KEY=` is a leftover export, not a credential. |
| 21 | fn env(&self, name: &str) -> Option<String>; |
| 22 | } |
| 23 | |
| 24 | /// The real process environment. |
| 25 | #[derive(Debug, Clone, Copy, Default)] |
| 26 | pub(crate) struct ProcessAuthContext; |
| 27 | |
| 28 | impl AuthContext for ProcessAuthContext { |
| 29 | fn env(&self, name: &str) -> Option<String> { |
| 30 | std::env::var(name) |
| 31 | .ok() |
| 32 | .filter(|value| !value.trim().is_empty()) |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | /// Test double: a fixed set of variables. |
| 37 | #[cfg(test)] |
| 38 | #[derive(Debug, Clone, Default)] |
| 39 | pub(crate) struct MapAuthContext { |
| 40 | env: BTreeMap<String, String>, |
| 41 | } |
| 42 | |
| 43 | #[cfg(test)] |
| 44 | impl MapAuthContext { |
| 45 | pub(crate) fn new() -> Self { |
| 46 | Self::default() |
| 47 | } |
| 48 | |
| 49 | pub(crate) fn with_env(mut self, name: &str, value: &str) -> Self { |
| 50 | self.env.insert(name.to_string(), value.to_string()); |
| 51 | self |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | #[cfg(test)] |
| 56 | impl AuthContext for MapAuthContext { |
| 57 | fn env(&self, name: &str) -> Option<String> { |
| 58 | self.env |
| 59 | .get(name) |
| 60 | .filter(|value| !value.trim().is_empty()) |
| 61 | .cloned() |
| 62 | } |
| 63 | } |
| 64 |