Repository navigation
Expand file tree
/
Copy pathtoken.go
More file actions
145 lines (127 loc) · 3.77 KB
/
Copy pathtoken.go
File metadata and controls
145 lines (127 loc) · 3.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
)
const (
copilotTokenURL = "https://api.github.com/copilot_internal/v2/token"
// Refresh the token this many seconds before it actually expires.
tokenRefreshBuffer = 60
)
// ghHostsFile is the default path for gh CLI OAuth token storage.
func ghHostsFile() string {
if p := os.Getenv("GH_CONFIG_DIR"); p != "" {
return filepath.Join(p, "hosts.yml")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "gh", "hosts.yml")
}
// ghHosts is the minimal structure we need from hosts.yml.
type ghHosts map[string]struct {
OAuthToken string `yaml:"oauth_token"`
}
// readGitHubPAT tries to find a GitHub PAT in order:
// 1. GITHUB_TOKEN env var
// 2. GH_TOKEN env var
// 3. gh CLI config (~/.config/gh/hosts.yml)
func readGitHubPAT() (string, error) {
if t := os.Getenv("GITHUB_TOKEN"); t != "" {
return t, nil
}
if t := os.Getenv("GH_TOKEN"); t != "" {
return t, nil
}
hostsPath := ghHostsFile()
data, err := os.ReadFile(hostsPath)
if err != nil {
return "", fmt.Errorf("no GITHUB_TOKEN set and cannot read %s: %w", hostsPath, err)
}
var hosts ghHosts
if err := yaml.Unmarshal(data, &hosts); err != nil {
return "", fmt.Errorf("parsing %s: %w", hostsPath, err)
}
for _, host := range []string{"github.com", "api.github.com"} {
if h, ok := hosts[host]; ok && h.OAuthToken != "" {
return h.OAuthToken, nil
}
}
return "", fmt.Errorf("no oauth_token found in %s", hostsPath)
}
// copilotTokenResponse is the JSON returned by the Copilot token endpoint.
type copilotTokenResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
}
// TokenManager caches a short-lived Copilot API token and refreshes it
// transparently before expiry.
type TokenManager struct {
mu sync.Mutex
token string
expiresAt time.Time
client *http.Client
}
// NewTokenManager creates a TokenManager and eagerly fetches the first token.
func NewTokenManager() (*TokenManager, error) {
tm := &TokenManager{
client: &http.Client{Timeout: 15 * time.Second},
}
if err := tm.refresh(); err != nil {
return nil, err
}
return tm, nil
}
// Token returns a valid Copilot API token, refreshing if necessary.
func (tm *TokenManager) Token() (string, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
if time.Now().Before(tm.expiresAt.Add(-tokenRefreshBuffer * time.Second)) {
return tm.token, nil
}
if err := tm.refresh(); err != nil {
return "", err
}
return tm.token, nil
}
// refresh fetches a new short-lived token from the GitHub API.
// Caller must hold tm.mu (or be in NewTokenManager before any goroutine sees tm).
func (tm *TokenManager) refresh() error {
pat, err := readGitHubPAT()
if err != nil {
return fmt.Errorf("reading GitHub PAT: %w", err)
}
req, err := http.NewRequest(http.MethodGet, copilotTokenURL, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "token "+pat)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "GitHubCopilotChat/0.26.7")
resp, err := tm.client.Do(req)
if err != nil {
return fmt.Errorf("fetching Copilot token: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Copilot token endpoint returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var tr copilotTokenResponse
if err := json.Unmarshal(body, &tr); err != nil {
return fmt.Errorf("parsing Copilot token response: %w", err)
}
if tr.Token == "" {
return fmt.Errorf("empty token in Copilot token response")
}
tm.token = tr.Token
tm.expiresAt = time.Unix(tr.ExpiresAt, 0)
logger.Printf("Copilot token refreshed, expires at %s", tm.expiresAt.Format(time.RFC3339))
return nil
}