返回 CodeWhale
auth.rs
根目录 / crates / config / src / route / auth.rs
1 //! Declarative auth types for the Route Contract (Phase 1).
2 //!
3 //! These types describe how a route asks for credentials. They do **not**
4 //! implement OAuth adapters. `AuthKind::OAuth` is a declared method only;
5 //! `UNIFIED_PROVIDER_LOGIN.md` remains gated.
6
7 use serde::{Deserialize, Serialize};
8
9 /// How a route obtains credentials.
10 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11 #[serde(rename_all = "kebab-case")]
12 pub enum AuthKind {
13 /// A provider-issued API key. Semantics are unchanged from today's first-class key path.
14 ApiKey,
15 /// An OAuth adapter *type*. Do not implement the Anthropic OAuth flow here.
16 #[serde(rename = "oauth")]
17 OAuth,
18 /// No credential required (local / keyless by default).
19 Keyless,
20 /// Read-only consent to an external CLI's credential file.
21 ExternalConsent,
22 }
23
24 /// A single declared auth method on a route.
25 ///
26 /// Phase 2 walks [`Self::prompts`] as one loop. Phase 1 only ships the types
27 /// and the export projection so the descriptor is complete.
28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29 pub struct AuthMethod {
30 /// Acquisition kind.
31 pub kind: AuthKind,
32 /// Human label for the picker / export.
33 pub label: &'static str,
34 /// Ordered prompts. Usually empty for a plain API-key route.
35 pub prompts: &'static [Prompt],
36 }
37
38 /// One question in a declarative auth prompt loop.
39 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
40 pub enum Prompt {
41 /// Free-text (or secret) answer stored under `key`.
42 Text {
43 /// Answer map key.
44 key: &'static str,
45 /// Prompt shown to the user.
46 message: &'static str,
47 /// Optional placeholder.
48 placeholder: Option<&'static str>,
49 /// When true, the answer is a secret and must not be echoed.
50 secret: bool,
51 /// Optional visibility predicate.
52 when: Option<When>,
53 },
54 /// Forced choice; `value` of the selected option is stored under `key`.
55 Select {
56 /// Answer map key.
57 key: &'static str,
58 /// Prompt shown to the user.
59 message: &'static str,
60 /// Options. A family-disambiguation `value` is a route id.
61 options: &'static [Choice],
62 /// Optional visibility predicate.
63 when: Option<When>,
64 },
65 }
66
67 /// Predicate over previously collected answers.
68 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
69 pub struct When {
70 /// Answer key to compare.
71 pub key: &'static str,
72 /// Comparison operator.
73 pub op: Op,
74 /// Comparison value.
75 pub value: &'static str,
76 }
77
78 /// Comparison used by [`When`].
79 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80 #[serde(rename_all = "kebab-case")]
81 pub enum Op {
82 /// Answer equals `value`.
83 Eq,
84 /// Answer does not equal `value`.
85 Neq,
86 }
87
88 /// One option in a [`Prompt::Select`].
89 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
90 pub struct Choice {
91 /// Display label.
92 pub label: &'static str,
93 /// Stored value. For family disambiguation this **is** a route id.
94 pub value: &'static str,
95 /// Optional hint.
96 pub hint: Option<&'static str>,
97 }
98
99 impl AuthMethod {
100 /// Plain API-key method with no extra prompts.
101 pub const API_KEY: Self = Self {
102 kind: AuthKind::ApiKey,
103 label: "API key",
104 prompts: &[],
105 };
106
107 /// Declared OAuth method. Adapter implementation is out of Phase 1 scope.
108 pub const OAUTH: Self = Self {
109 kind: AuthKind::OAuth,
110 label: "OAuth",
111 prompts: &[],
112 };
113
114 /// Keyless / local-optional method.
115 pub const KEYLESS: Self = Self {
116 kind: AuthKind::Keyless,
117 label: "Keyless",
118 prompts: &[],
119 };
120
121 /// External-consent method (read-only grant to another CLI's file).
122 pub const EXTERNAL_CONSENT: Self = Self {
123 kind: AuthKind::ExternalConsent,
124 label: "External consent",
125 prompts: &[],
126 };
127
128 /// Whether `answers` satisfy this prompt's `when` clause.
129 #[must_use]
130 pub fn prompt_visible(
131 prompt: &Prompt,
132 answers: &std::collections::BTreeMap<String, String>,
133 ) -> bool {
134 let when = match prompt {
135 Prompt::Text { when, .. } | Prompt::Select { when, .. } => *when,
136 };
137 match when {
138 None => true,
139 Some(When { key, op, value }) => {
140 let actual = answers.get(key).map(String::as_str).unwrap_or("");
141 match op {
142 Op::Eq => actual == value,
143 Op::Neq => actual != value,
144 }
145 }
146 }
147 }
148 }
149
150 /// Owned, serializable projection of [`AuthMethod`] for `providers export`.
151 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152 #[serde(rename_all = "camelCase")]
153 pub struct AuthMethodExport {
154 /// Acquisition kind.
155 pub kind: AuthKind,
156 /// Human label.
157 pub label: String,
158 /// Owned prompts.
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub prompts: Vec<PromptExport>,
161 }
162
163 /// Owned, serializable projection of [`Prompt`].
164 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165 #[serde(tag = "type", rename_all = "camelCase")]
166 pub enum PromptExport {
167 /// Free-text prompt.
168 Text {
169 /// Answer map key.
170 key: String,
171 /// Prompt shown to the user.
172 message: String,
173 /// Optional placeholder.
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 placeholder: Option<String>,
176 /// Whether the answer is a secret.
177 secret: bool,
178 /// Optional visibility predicate.
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 when: Option<WhenExport>,
181 },
182 /// Forced-choice prompt.
183 Select {
184 /// Answer map key.
185 key: String,
186 /// Prompt shown to the user.
187 message: String,
188 /// Options.
189 options: Vec<ChoiceExport>,
190 /// Optional visibility predicate.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 when: Option<WhenExport>,
193 },
194 }
195
196 /// Owned [`When`].
197 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198 #[serde(rename_all = "camelCase")]
199 pub struct WhenExport {
200 /// Answer key to compare.
201 pub key: String,
202 /// Comparison operator.
203 pub op: Op,
204 /// Comparison value.
205 pub value: String,
206 }
207
208 /// Owned [`Choice`].
209 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210 #[serde(rename_all = "camelCase")]
211 pub struct ChoiceExport {
212 /// Display label.
213 pub label: String,
214 /// Stored value.
215 pub value: String,
216 /// Optional hint.
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub hint: Option<String>,
219 }
220
221 impl From<AuthMethod> for AuthMethodExport {
222 fn from(value: AuthMethod) -> Self {
223 Self {
224 kind: value.kind,
225 label: value.label.to_string(),
226 prompts: value
227 .prompts
228 .iter()
229 .copied()
230 .map(PromptExport::from)
231 .collect(),
232 }
233 }
234 }
235
236 impl From<Prompt> for PromptExport {
237 fn from(value: Prompt) -> Self {
238 match value {
239 Prompt::Text {
240 key,
241 message,
242 placeholder,
243 secret,
244 when,
245 } => Self::Text {
246 key: key.to_string(),
247 message: message.to_string(),
248 placeholder: placeholder.map(str::to_string),
249 secret,
250 when: when.map(WhenExport::from),
251 },
252 Prompt::Select {
253 key,
254 message,
255 options,
256 when,
257 } => Self::Select {
258 key: key.to_string(),
259 message: message.to_string(),
260 options: options.iter().copied().map(ChoiceExport::from).collect(),
261 when: when.map(WhenExport::from),
262 },
263 }
264 }
265 }
266
267 impl From<When> for WhenExport {
268 fn from(value: When) -> Self {
269 Self {
270 key: value.key.to_string(),
271 op: value.op,
272 value: value.value.to_string(),
273 }
274 }
275 }
276
277 impl From<Choice> for ChoiceExport {
278 fn from(value: Choice) -> Self {
279 Self {
280 label: value.label.to_string(),
281 value: value.value.to_string(),
282 hint: value.hint.map(str::to_string),
283 }
284 }
285 }
286
287 #[cfg(test)]
288 mod tests {
289 use super::*;
290
291 #[test]
292 fn when_eq_hides_unmatched_prompt() {
293 let prompt = Prompt::Text {
294 key: "key",
295 message: "Key",
296 placeholder: None,
297 secret: true,
298 when: Some(When {
299 key: "plan",
300 op: Op::Eq,
301 value: "coding",
302 }),
303 };
304 let mut answers = std::collections::BTreeMap::new();
305 answers.insert("plan".into(), "token".into());
306 assert!(!AuthMethod::prompt_visible(&prompt, &answers));
307 answers.insert("plan".into(), "coding".into());
308 assert!(AuthMethod::prompt_visible(&prompt, &answers));
309 }
310 }
311
311 lines RUST