@@ -0,0 +1,199 @@
13+ "github.com/charmbracelet/crush/internal/oauth"
17+ clientID = "Iv1.b507a08c87ecfe98"
19+ deviceCodeURL = "https://github.com/login/device/code"
20+ accessTokenURL = "https://github.com/login/oauth/access_token"
21+ copilotTokenURL = "https://api.github.com/copilot_internal/v2/token"
24+type DeviceCode struct {
25+ DeviceCode string `json:"device_code"`
26+ UserCode string `json:"user_code"`
27+ VerificationURI string `json:"verification_uri"`
28+ ExpiresIn int `json:"expires_in"`
29+ Interval int `json:"interval"`
32+// RequestDeviceCode initiates the device code flow with GitHub.
33+func RequestDeviceCode(ctx context.Context) (*DeviceCode, error) {
35+ data.Set("client_id", clientID)
36+ data.Set("scope", "read:user")
38+ req, err := http.NewRequestWithContext(ctx, "POST", deviceCodeURL, strings.NewReader(data.Encode()))
42+ req.Header.Set("Accept", "application/json")
43+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
44+ req.Header.Set("User-Agent", userAgent)
46+ client := &http.Client{Timeout: 30 * time.Second}
47+ resp, err := client.Do(req)
51+ defer resp.Body.Close()
53+ if resp.StatusCode != http.StatusOK {
54+ body, _ := io.ReadAll(resp.Body)
55+ return nil, fmt.Errorf("device code request failed: %s - %s", resp.Status, string(body))
59+ if err := json.NewDecoder(resp.Body).Decode(&dc); err != nil {
65+// PollForToken polls GitHub for the access token after user authorization.
66+func PollForToken(ctx context.Context, dc *DeviceCode) (*oauth.Token, error) {
67+ interval := max(dc.Interval, 5)
68+ deadline := time.Now().Add(time.Duration(dc.ExpiresIn) * time.Second)
69+ ticker := time.NewTicker(time.Duration(interval) * time.Second)
72+ for time.Now().Before(deadline) {
75+ return nil, ctx.Err()
79+ token, err := tryGetToken(ctx, dc.DeviceCode)
80+ if err == errPending {
83+ if err == errSlowDown {
85+ ticker.Reset(time.Duration(interval) * time.Second)
94+ return nil, fmt.Errorf("authorization timed out")
98+ errPending = fmt.Errorf("pending")
99+ errSlowDown = fmt.Errorf("slow_down")
102+func tryGetToken(ctx context.Context, deviceCode string) (*oauth.Token, error) {
103+ data := url.Values{}
104+ data.Set("client_id", clientID)
105+ data.Set("device_code", deviceCode)
106+ data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
108+ req, err := http.NewRequestWithContext(ctx, "POST", accessTokenURL, strings.NewReader(data.Encode()))
112+ req.Header.Set("Accept", "application/json")
113+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
114+ req.Header.Set("User-Agent", userAgent)
116+ client := &http.Client{Timeout: 30 * time.Second}
117+ resp, err := client.Do(req)
121+ defer resp.Body.Close()
124+ AccessToken string `json:"access_token"`
125+ Error string `json:"error"`
127+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
131+ switch result.Error {
133+ if result.AccessToken == "" {
134+ return nil, errPending
136+ return getCopilotToken(ctx, result.AccessToken)
137+ case "authorization_pending":
138+ return nil, errPending
140+ return nil, errSlowDown
142+ return nil, fmt.Errorf("authorization failed: %s", result.Error)
146+func getCopilotToken(ctx context.Context, githubToken string) (*oauth.Token, error) {
147+ req, err := http.NewRequestWithContext(ctx, "GET", copilotTokenURL, nil)
151+ req.Header.Set("Accept", "application/json")
152+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", githubToken))
153+ for k, v := range Headers() {
154+ req.Header.Set(k, v)
157+ client := &http.Client{Timeout: 30 * time.Second}
158+ resp, err := client.Do(req)
162+ defer resp.Body.Close()
164+ body, err := io.ReadAll(resp.Body)
169+ if resp.StatusCode != http.StatusOK {
170+ if resp.StatusCode == http.StatusNotFound {
171+ return nil, fmt.Errorf("copilot not available for this account\n\n" +
172+ "Please ensure you have GitHub Copilot enabled at:\n" +
173+ "https://github.com/settings/copilot")
175+ return nil, fmt.Errorf("copilot token request failed: %s - %s", resp.Status, string(body))
179+ Token string `json:"token"`
180+ ExpiresAt int64 `json:"expires_at"`
182+ if err := json.Unmarshal(body, &result); err != nil {
186+ copilotToken := &oauth.Token{
187+ AccessToken: result.Token,
188+ RefreshToken: githubToken,
189+ ExpiresAt: result.ExpiresAt,
191+ copilotToken.SetExpiresIn()
193+ return copilotToken, nil
196+// RefreshToken refreshes the Copilot token using the GitHub token.
197+func RefreshToken(ctx context.Context, githubToken string) (*oauth.Token, error) {
198+ return getCopilotToken(ctx, githubToken)