返回 CodeWhale
mod.rs
根目录 / crates / tui / src / credentials / mod.rs
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
139 lines RUST