| 1 | //! Provider credentials: one type-tagged credential per provider, one write |
| 2 | //! path, and one resolution result that names its source. |
| 3 | //! |
| 4 | //! # Attribution |
| 5 | //! |
| 6 | //! The design of this module is ported from **pi-mono** by Mario Zechner |
| 7 | //! (<https://github.com/earendil-works/pi-mono>), MIT licensed, specifically: |
| 8 | //! |
| 9 | //! | pi-mono file | ported into | |
| 10 | //! |-------------------------------------------|------------------------------------------------| |
| 11 | //! | `packages/ai/src/auth/types.ts` | [`Credential`], [`CredentialStore`], [`AuthContext`] | |
| 12 | //! | `packages/ai/src/auth/credential-store.ts`| [`store::InMemoryCredentialStore`] | |
| 13 | //! | `packages/ai/src/auth/context.ts` | [`context::ProcessAuthContext`] | |
| 14 | //! | `packages/ai/src/auth/resolve.ts` | `crate::config::credential_resolve` | |
| 15 | //! |
| 16 | //! This is a **design port into idiomatic Rust, not a line-for-line copy**. |
| 17 | //! pi's module is async TypeScript over a `Provider` record with a single |
| 18 | //! `auth.json`; CodeWhale's is synchronous Rust over `ApiProvider` and the |
| 19 | //! several pre-existing on-disk stores (secret store, config file, ambient |
| 20 | //! environment, externally consented CLI credential files). The four ideas |
| 21 | //! taken verbatim in spirit are: one type-tagged credential per provider, |
| 22 | //! `modify` as the only serialized write path, one stated precedence rule in |
| 23 | //! one place, and every resolution carrying a human-readable source label. |
| 24 | //! Several doc comments are adapted closely enough that the MIT permission |
| 25 | //! notice travels with them, reproduced below. |
| 26 | //! |
| 27 | //! ```text |
| 28 | //! MIT License |
| 29 | //! |
| 30 | //! Copyright (c) 2025 Mario Zechner |
| 31 | //! |
| 32 | //! Permission is hereby granted, free of charge, to any person obtaining a copy |
| 33 | //! of this software and associated documentation files (the "Software"), to deal |
| 34 | //! in the Software without restriction, including without limitation the rights |
| 35 | //! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 36 | //! copies of the Software, and to permit persons to whom the Software is |
| 37 | //! furnished to do so, subject to the following conditions: |
| 38 | //! |
| 39 | //! The above copyright notice and this permission notice shall be included in all |
| 40 | //! copies or substantial portions of the Software. |
| 41 | //! |
| 42 | //! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 43 | //! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 44 | //! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 45 | //! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 46 | //! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 47 | //! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 48 | //! SOFTWARE. |
| 49 | //! ``` |
| 50 | //! |
| 51 | //! # Redaction |
| 52 | //! |
| 53 | //! Nothing in this module renders or logs secret material. [`Credential`] |
| 54 | //! deliberately has a hand-written [`std::fmt::Debug`] that prints only the |
| 55 | //! variant tag, so a credential cannot reach a log line through `{:?}` on a |
| 56 | //! surrounding struct. |
| 57 | |
| 58 | pub(crate) mod context; |
| 59 | pub(crate) mod source; |
| 60 | pub(crate) mod store; |
| 61 | |
| 62 | #[cfg(test)] |
| 63 | mod tests; |
| 64 | |
| 65 | pub(crate) use context::AuthContext; |
| 66 | pub(crate) use source::{CredentialProbe, CredentialResolution, CredentialSource}; |
| 67 | pub(crate) use store::{CredentialInfo, CredentialStore}; |
| 68 | |
| 69 | /// One type-tagged credential per provider — pi's `Credential` union. |
| 70 | /// |
| 71 | /// CodeWhale stores API keys in the secret store and OAuth material in |
| 72 | /// provider-specific files that this type deliberately does **not** try to |
| 73 | /// unify; the OAuth variant carries only what a status surface needs, so |
| 74 | /// adopting this type never moves a token between stores. |
| 75 | #[derive(Clone, PartialEq, Eq)] |
| 76 | pub(crate) enum Credential { |
| 77 | /// A bearer API key held in CodeWhale's own durable store. |
| 78 | ApiKey { key: String }, |
| 79 | /// An OAuth access token plus its expiry, if the flow reported one. |
| 80 | /// |
| 81 | /// Constructed today only by this module's own tests: CodeWhale's OAuth |
| 82 | /// stores (xAI's generation files, the read-only external grants) have not |
| 83 | /// been moved behind [`CredentialStore`] in this change, so nothing in the |
| 84 | /// production path mints one yet. The variant is kept because it is half |
| 85 | /// of the ported contract and the store's serialization guarantee exists |
| 86 | /// precisely for it. |
| 87 | #[cfg_attr(not(test), expect(dead_code))] |
| 88 | OAuth { |
| 89 | access: String, |
| 90 | expires_at_unix_secs: Option<i64>, |
| 91 | }, |
| 92 | } |
| 93 | |
| 94 | impl Credential { |
| 95 | /// Only [`store::InMemoryCredentialStore::list`] needs this today; the |
| 96 | /// secret-store adapter knows every slot it holds is an api key. |
| 97 | pub(crate) fn kind(&self) -> CredentialKind { |
| 98 | match self { |
| 99 | Self::ApiKey { .. } => CredentialKind::ApiKey, |
| 100 | Self::OAuth { .. } => CredentialKind::OAuth, |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /// Borrow the secret. Callers must not log or render the result; this is |
| 105 | /// the single narrow accessor so `rg "expose_secret"` finds every use. |
| 106 | pub(crate) fn expose_secret(&self) -> &str { |
| 107 | match self { |
| 108 | Self::ApiKey { key } => key, |
| 109 | Self::OAuth { access, .. } => access, |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// Never print secret material, even through a derived `Debug` on a |
| 115 | /// surrounding struct. |
| 116 | impl std::fmt::Debug for Credential { |
| 117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 118 | match self { |
| 119 | Self::ApiKey { .. } => f.write_str("Credential::ApiKey(<redacted>)"), |
| 120 | Self::OAuth { |
| 121 | expires_at_unix_secs, |
| 122 | .. |
| 123 | } => f |
| 124 | .debug_struct("Credential::OAuth") |
| 125 | .field("access", &"<redacted>") |
| 126 | .field("expires_at_unix_secs", expires_at_unix_secs) |
| 127 | .finish(), |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 133 | pub(crate) enum CredentialKind { |
| 134 | ApiKey, |
| 135 | /// See the note on [`Credential::OAuth`]: no production store mints one |
| 136 | /// yet. |
| 137 | OAuth, |
| 138 | } |
| 139 |