aiclient_api/auth/
copilot.rs1use anyhow::{bail, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4use tokio::time::sleep;
5
6use super::{TokenData, TokenStore};
7
8const CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
9const DEVICE_CODE_URL: &str = "https://github.com/login/device/code";
10const ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
11const COPILOT_TOKEN_URL: &str =
12 "https://api.github.com/copilot_internal/v2/token";
13
14#[derive(Debug, Deserialize)]
15struct DeviceCodeResponse {
16 device_code: String,
17 user_code: String,
18 verification_uri: String,
19 interval: u64,
20}
21
22#[derive(Debug, Deserialize)]
23struct AccessTokenResponse {
24 access_token: Option<String>,
25 error: Option<String>,
26}
27
28#[derive(Debug, Deserialize, Serialize)]
29pub struct CopilotTokenResponse {
30 pub token: String,
31 pub expires_at: i64,
32 pub refresh_in: u64,
33}
34
35pub async fn authenticate(store: &dyn TokenStore) -> Result<()> {
36 let client = reqwest::Client::new();
37
38 let body = serde_json::json!({
40 "client_id": CLIENT_ID,
41 "scope": "read:user"
42 });
43
44 let device_resp: DeviceCodeResponse = client
45 .post(DEVICE_CODE_URL)
46 .header("content-type", "application/json")
47 .header("accept", "application/json")
48 .json(&body)
49 .send()
50 .await
51 .context("Failed to request device code")?
52 .json()
53 .await
54 .context("Failed to parse device code response")?;
55
56 println!(
58 "\nPlease visit: {}",
59 device_resp.verification_uri
60 );
61 println!("And enter code: {}", device_resp.user_code);
62 println!("\nAttempting to open browser...");
63
64 let _ = open::that(&device_resp.verification_uri);
65
66 let mut poll_interval = Duration::from_secs(device_resp.interval + 1);
68 let poll_body = serde_json::json!({
69 "client_id": CLIENT_ID,
70 "device_code": device_resp.device_code,
71 "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
72 });
73
74 println!("\nWaiting for authorization...");
75
76 loop {
77 sleep(poll_interval).await;
78
79 let token_resp: AccessTokenResponse = client
80 .post(ACCESS_TOKEN_URL)
81 .header("content-type", "application/json")
82 .header("accept", "application/json")
83 .json(&poll_body)
84 .send()
85 .await
86 .context("Failed to poll for access token")?
87 .json()
88 .await
89 .context("Failed to parse access token response")?;
90
91 if let Some(access_token) = token_resp.access_token {
92 let token_data = TokenData::Copilot {
94 github_token: access_token,
95 copilot_token: None,
96 expires_at: None,
97 };
98 store
99 .save("copilot", &token_data)
100 .await
101 .context("Failed to save token")?;
102 return Ok(());
103 }
104
105 match token_resp.error.as_deref() {
106 Some("authorization_pending") => {
107 }
109 Some("slow_down") => {
110 poll_interval += Duration::from_secs(5);
111 }
112 Some("expired_token") => {
113 bail!("Device code expired. Please try again.");
114 }
115 Some("access_denied") => {
116 bail!("Authorization denied by user.");
117 }
118 Some(err) => {
119 bail!("Unexpected error: {}", err);
120 }
121 None => {
122 bail!("Unexpected empty response from OAuth server");
123 }
124 }
125 }
126}
127
128pub async fn fetch_copilot_token(
129 client: &reqwest::Client,
130 github_token: &str,
131) -> Result<CopilotTokenResponse> {
132 let resp = client
133 .get(COPILOT_TOKEN_URL)
134 .header("authorization", format!("token {}", github_token))
135 .header("content-type", "application/json")
136 .header("accept", "application/json")
137 .header("editor-version", "vscode/1.110.1")
138 .header("editor-plugin-version", "copilot-chat/0.38.2")
139 .header("user-agent", "GitHubCopilotChat/0.38.2")
140 .header("x-github-api-version", "2025-10-01")
141 .header("x-vscode-user-agent-library-version", "electron-fetch")
142 .send()
143 .await
144 .context("Failed to fetch Copilot token")?;
145
146 if !resp.status().is_success() {
147 let status = resp.status();
148 let body = resp.text().await.unwrap_or_default();
149 bail!("Failed to fetch Copilot token: HTTP {} - {}", status, body);
150 }
151
152 let token_resp: CopilotTokenResponse = resp
153 .json()
154 .await
155 .context("Failed to parse Copilot token response")?;
156
157 Ok(token_resp)
158}