1use std::collections::HashMap;
4use std::future::Future;
5use std::sync::{Arc, OnceLock, Weak};
6
7use async_trait::async_trait;
8use parking_lot::Mutex;
9use serde_json::Value;
10
11use crate::generated::api_types::{
12 GitHubTokenAcquireReason, GitHubTokenAcquireRequest, GitHubTokenAcquireResult,
13 GitHubTokenAcquireResultCancelled, GitHubTokenAcquireResultToken,
14};
15use crate::{Client, ClientInner, JsonRpcError, JsonRpcRequest, JsonRpcResponse, error_codes};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum GitHubTokenRequestReason {
20 Initial,
22 Refresh,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct GitHubTokenProviderArgs {
29 pub host: String,
31 pub session_id: Option<crate::SessionId>,
33 pub reason: GitHubTokenRequestReason,
35}
36
37pub struct GitHubToken {
42 access_token: String,
43 expires_in_seconds: i64,
44 token_type: Option<String>,
45}
46
47impl GitHubToken {
48 pub fn new(access_token: impl Into<String>, expires_in_seconds: i64) -> Self {
50 Self {
51 access_token: access_token.into(),
52 expires_in_seconds,
53 token_type: None,
54 }
55 }
56
57 pub fn with_token_type(mut self, token_type: impl Into<String>) -> Self {
59 self.token_type = Some(token_type.into());
60 self
61 }
62
63 fn into_wire(self) -> GitHubTokenAcquireResultToken {
64 GitHubTokenAcquireResultToken {
65 access_token: self.access_token,
66 expires_in: self.expires_in_seconds,
67 kind: Default::default(),
68 token_type: self.token_type,
69 }
70 }
71}
72
73impl std::fmt::Debug for GitHubToken {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("GitHubToken")
76 .field("access_token", &"<redacted>")
77 .field("expires_in_seconds", &self.expires_in_seconds)
78 .field("token_type", &self.token_type)
79 .finish()
80 }
81}
82
83pub enum GitHubTokenProviderResult {
85 Token(GitHubToken),
87 Cancelled,
89}
90
91impl std::fmt::Debug for GitHubTokenProviderResult {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::Token(token) => f.debug_tuple("Token").field(token).finish(),
95 Self::Cancelled => f.write_str("Cancelled"),
96 }
97 }
98}
99
100#[async_trait]
102pub trait GitHubTokenProvider: Send + Sync {
103 async fn get_token(
108 &self,
109 args: GitHubTokenProviderArgs,
110 ) -> Result<GitHubTokenProviderResult, crate::Error>;
111}
112
113#[async_trait]
114impl<F, Fut> GitHubTokenProvider for F
115where
116 F: Fn(GitHubTokenProviderArgs) -> Fut + Send + Sync,
117 Fut: Future<Output = Result<GitHubTokenProviderResult, crate::Error>> + Send,
118{
119 async fn get_token(
120 &self,
121 args: GitHubTokenProviderArgs,
122 ) -> Result<GitHubTokenProviderResult, crate::Error> {
123 (self)(args).await
124 }
125}
126
127#[derive(Default)]
128struct RegistryState {
129 providers: HashMap<String, Arc<dyn GitHubTokenProvider>>,
130 session_owners: HashMap<crate::SessionId, String>,
131}
132
133pub(crate) struct GitHubTokenRegistry {
134 state: Mutex<RegistryState>,
135 client: OnceLock<Weak<ClientInner>>,
136}
137
138impl GitHubTokenRegistry {
139 pub(crate) fn new() -> Self {
140 Self {
141 state: Mutex::new(RegistryState::default()),
142 client: OnceLock::new(),
143 }
144 }
145
146 pub(crate) fn set_client(&self, client: Weak<ClientInner>) {
147 let _ = self.client.set(client);
148 }
149
150 pub(crate) fn register(&self, provider: Arc<dyn GitHubTokenProvider>) -> String {
151 let registration_id = uuid::Uuid::new_v4().to_string();
152 self.state
153 .lock()
154 .providers
155 .insert(registration_id.clone(), provider);
156 registration_id
157 }
158
159 pub(crate) fn claim(&self, registration_id: &str, session_id: crate::SessionId) {
160 let mut state = self.state.lock();
161 if let Some(previous) = state
162 .session_owners
163 .insert(session_id, registration_id.to_string())
164 && previous != registration_id
165 {
166 state.providers.remove(&previous);
167 }
168 }
169
170 pub(crate) fn unregister(&self, registration_id: &str) {
171 let mut state = self.state.lock();
172 state.providers.remove(registration_id);
173 state
174 .session_owners
175 .retain(|_, owned| owned != registration_id);
176 }
177
178 pub(crate) fn retire_session(&self, session_id: &crate::SessionId) {
179 let mut state = self.state.lock();
180 if let Some(registration_id) = state.session_owners.remove(session_id) {
181 state.providers.remove(®istration_id);
182 }
183 }
184
185 pub(crate) fn clear(&self) {
186 let mut state = self.state.lock();
187 state.providers.clear();
188 state.session_owners.clear();
189 }
190
191 pub(crate) async fn dispatch(&self, request: JsonRpcRequest) {
192 let Some(inner) = self.client.get().and_then(Weak::upgrade) else {
193 return;
194 };
195 let client = Client::from_inner(inner);
196 let params = request
197 .params
198 .clone()
199 .unwrap_or(Value::Object(serde_json::Map::new()));
200 let params: GitHubTokenAcquireRequest = match serde_json::from_value(params) {
201 Ok(params) => params,
202 Err(error) => {
203 send_error(
204 &client,
205 request.id,
206 error_codes::INVALID_PARAMS,
207 &format!("invalid params: {error}"),
208 )
209 .await;
210 return;
211 }
212 };
213 let provider = self
214 .state
215 .lock()
216 .providers
217 .get(¶ms.registration_id)
218 .cloned();
219 let Some(provider) = provider else {
220 send_error(
221 &client,
222 request.id,
223 error_codes::INTERNAL_ERROR,
224 "unknown GitHub token provider registration",
225 )
226 .await;
227 return;
228 };
229
230 let reason = match params.reason {
231 GitHubTokenAcquireReason::Initial => GitHubTokenRequestReason::Initial,
232 GitHubTokenAcquireReason::Refresh => GitHubTokenRequestReason::Refresh,
233 GitHubTokenAcquireReason::Unknown => {
234 send_error(
235 &client,
236 request.id,
237 error_codes::INVALID_PARAMS,
238 "unknown GitHub token acquisition reason",
239 )
240 .await;
241 return;
242 }
243 };
244
245 match provider
246 .get_token(GitHubTokenProviderArgs {
247 host: params.host,
248 session_id: params.session_id,
249 reason,
250 })
251 .await
252 {
253 Ok(GitHubTokenProviderResult::Token(token)) => {
254 respond(
255 &client,
256 request.id,
257 GitHubTokenAcquireResult::Token(token.into_wire()),
258 )
259 .await;
260 }
261 Ok(GitHubTokenProviderResult::Cancelled) => {
262 respond(
263 &client,
264 request.id,
265 GitHubTokenAcquireResult::Cancelled(GitHubTokenAcquireResultCancelled {
266 kind: Default::default(),
267 }),
268 )
269 .await;
270 }
271 Err(error) => {
272 send_error(
273 &client,
274 request.id,
275 error_codes::INTERNAL_ERROR,
276 &format!("GitHub token provider failed: {error}"),
277 )
278 .await;
279 }
280 }
281 }
282}
283
284pub(crate) struct GitHubTokenRegistration {
285 registry: Arc<GitHubTokenRegistry>,
286 id: String,
287}
288
289impl GitHubTokenRegistration {
290 pub(crate) fn new(registry: Arc<GitHubTokenRegistry>, id: String) -> Self {
291 Self { registry, id }
292 }
293
294 pub(crate) fn id(&self) -> &str {
295 &self.id
296 }
297
298 pub(crate) fn claim(&self, session_id: crate::SessionId) {
299 self.registry.claim(&self.id, session_id);
300 }
301}
302
303impl Drop for GitHubTokenRegistration {
304 fn drop(&mut self) {
305 self.registry.unregister(&self.id);
306 }
307}
308
309async fn respond(client: &Client, request_id: u64, result: GitHubTokenAcquireResult) {
310 match serde_json::to_value(result) {
311 Ok(result) => {
312 let _ = client
313 .send_response(&JsonRpcResponse {
314 jsonrpc: "2.0".to_string(),
315 id: request_id,
316 result: Some(result),
317 error: None,
318 })
319 .await;
320 }
321 Err(_) => {
322 send_error(
323 client,
324 request_id,
325 error_codes::INTERNAL_ERROR,
326 "serialization failure",
327 )
328 .await;
329 }
330 }
331}
332
333async fn send_error(client: &Client, request_id: u64, code: i32, message: &str) {
334 let _ = client
335 .send_response(&JsonRpcResponse {
336 jsonrpc: "2.0".to_string(),
337 id: request_id,
338 result: None,
339 error: Some(JsonRpcError {
340 code,
341 message: message.to_string(),
342 data: None,
343 }),
344 })
345 .await;
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn token_debug_is_redacted() {
354 let token = GitHubToken::new("do-not-print", 28_800);
355 assert!(!format!("{token:?}").contains("do-not-print"));
356 }
357
358 #[test]
359 fn retiring_session_removes_its_provider() {
360 let registry = GitHubTokenRegistry::new();
361 let provider = Arc::new(|_args: GitHubTokenProviderArgs| async {
362 Ok(GitHubTokenProviderResult::Cancelled)
363 });
364 let registration_id = registry.register(provider);
365 let session_id = crate::SessionId::from("session-1");
366 registry.claim(®istration_id, session_id.clone());
367
368 registry.retire_session(&session_id);
369
370 assert!(
371 !registry
372 .state
373 .lock()
374 .providers
375 .contains_key(®istration_id)
376 );
377 }
378}