Skip to content

Commit 9e7a1a4

Browse files
committed
feat(copilot/auth): implement GitHub Copilot authentication flow
- Add device-code OAuth flow with GitHub token exchange - Implement Copilot token acquisition and refresh logic - Add account type handling (individual/business/enterprise) - Add token persistence and storage management - Add CLI login command (cliproxy-api copilot login) - Register Copilot refresh handler in SDK - Validate account_type with warning for invalid values
1 parent 6d79891 commit 9e7a1a4

File tree

9 files changed

+1139
-1
lines changed

9 files changed

+1139
-1
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Package copilot provides authentication and token management for GitHub Copilot API.
2+
// It handles the OAuth2 device code flow, token exchange, and automatic token refresh.
3+
package copilot
4+
5+
import (
6+
"crypto/rand"
7+
"encoding/hex"
8+
"fmt"
9+
10+
copilotshared "github.com/router-for-me/CLIProxyAPI/v6/internal/copilot"
11+
)
12+
13+
const (
14+
GitHubBaseURL = "https://github.com"
15+
GitHubAPIBaseURL = "https://api.github.com"
16+
// GitHubClientID is the PUBLIC OAuth client ID for GitHub Copilot's VS Code extension.
17+
// This is NOT a secret - it's the same client ID used by the official Copilot CLI and
18+
// VS Code extension, publicly visible in their source code and network requests.
19+
GitHubClientID = "Iv1.b507a08c87ecfe98"
20+
GitHubAppScopes = "read:user"
21+
DeviceCodePath = "/login/device/code"
22+
AccessTokenPath = "/login/oauth/access_token"
23+
CopilotTokenPath = "/copilot_internal/v2/token"
24+
CopilotUserPath = "/copilot_internal/user"
25+
UserInfoPath = "/user"
26+
27+
CopilotVersion = "0.0.363"
28+
EditorPluginVersion = "copilot/" + CopilotVersion
29+
CopilotUserAgent = "copilot/" + CopilotVersion + " (linux v22.15.0)"
30+
CopilotAPIVersion = "2025-05-01"
31+
CopilotIntegrationID = "copilot-developer-cli"
32+
DefaultVSCodeVersion = "1.95.0"
33+
)
34+
35+
type AccountType = copilotshared.AccountType
36+
37+
const (
38+
AccountTypeIndividual AccountType = copilotshared.AccountTypeIndividual
39+
AccountTypeBusiness AccountType = copilotshared.AccountTypeBusiness
40+
AccountTypeEnterprise AccountType = copilotshared.AccountTypeEnterprise
41+
)
42+
43+
var ValidAccountTypes = copilotshared.ValidAccountTypes
44+
45+
const DefaultAccountType = copilotshared.DefaultAccountType
46+
47+
func CopilotBaseURL(accountType AccountType) string {
48+
switch accountType {
49+
case AccountTypeBusiness:
50+
return "https://api.business.githubcopilot.com"
51+
case AccountTypeEnterprise:
52+
return "https://api.enterprise.githubcopilot.com"
53+
default:
54+
// Individual accounts use the individual Copilot endpoint.
55+
return "https://api.individual.githubcopilot.com"
56+
}
57+
}
58+
59+
func StandardHeaders() map[string]string {
60+
return map[string]string{
61+
"Content-Type": "application/json",
62+
"Accept": "application/json",
63+
}
64+
}
65+
66+
func GitHubHeaders(githubToken, vsCodeVersion string) map[string]string {
67+
if vsCodeVersion == "" {
68+
vsCodeVersion = DefaultVSCodeVersion
69+
}
70+
return map[string]string{
71+
"Content-Type": "application/json",
72+
"Accept": "application/json",
73+
"Authorization": fmt.Sprintf("token %s", githubToken),
74+
"Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion),
75+
"Editor-Plugin-Version": EditorPluginVersion,
76+
"User-Agent": CopilotUserAgent,
77+
"X-Github-Api-Version": CopilotAPIVersion,
78+
"X-Vscode-User-Agent-Library-Version": "electron-fetch",
79+
}
80+
}
81+
82+
func CopilotHeaders(copilotToken, vsCodeVersion string, enableVision bool) map[string]string {
83+
if vsCodeVersion == "" {
84+
vsCodeVersion = DefaultVSCodeVersion
85+
}
86+
headers := map[string]string{
87+
"Content-Type": "application/json",
88+
"Authorization": fmt.Sprintf("Bearer %s", copilotToken),
89+
"Copilot-Integration-Id": CopilotIntegrationID,
90+
"Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion),
91+
"Editor-Plugin-Version": EditorPluginVersion,
92+
"User-Agent": CopilotUserAgent,
93+
"Openai-Intent": "conversation-agent",
94+
"X-Github-Api-Version": CopilotAPIVersion,
95+
"X-Request-Id": generateRequestID(),
96+
"X-Interaction-Id": generateRequestID(),
97+
"X-Vscode-User-Agent-Library-Version": "electron-fetch",
98+
}
99+
if enableVision {
100+
headers["Copilot-Vision-Request"] = "true"
101+
}
102+
return headers
103+
}
104+
105+
func generateRequestID() string {
106+
b := make([]byte, 16)
107+
if _, err := rand.Read(b); err != nil {
108+
panic(fmt.Sprintf("failed to generate random bytes for request ID: %v", err))
109+
}
110+
return fmt.Sprintf("%s-%s-%s-%s-%s",
111+
hex.EncodeToString(b[0:4]),
112+
hex.EncodeToString(b[4:6]),
113+
hex.EncodeToString(b[6:8]),
114+
hex.EncodeToString(b[8:10]),
115+
hex.EncodeToString(b[10:16]))
116+
}
117+
118+
// MaskToken returns a masked version of a token for safe logging.
119+
// Shows first 2 and last 2 characters with asterisks in between.
120+
// Returns "<empty>" for empty tokens and "<short>" for tokens under 5 chars.
121+
func MaskToken(token string) string {
122+
if token == "" {
123+
return "<empty>"
124+
}
125+
if len(token) < 5 {
126+
return "<short>"
127+
}
128+
return token[:2] + "****" + token[len(token)-2:]
129+
}

0 commit comments

Comments
 (0)