From 2c296e9cb19ef48bf2b8f51911ab4fd7bf115b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Mart=C3=ADnez?= Date: Fri, 28 Nov 2025 08:32:36 +0100 Subject: [PATCH] refactor(copilot): improve code quality in authentication module - Add Unwrap() to AuthenticationError for proper error chain handling with errors.Is/As - Extract hardcoded header values to constants for maintainability - Replace verbose status code checks with isHTTPSuccess() helper - Remove unused ExtractBearerToken() and BuildModelsURL() functions - Make buildChatCompletionURL() private (only used internally) - Remove unused 'strings' import --- internal/auth/copilot/copilot_auth.go | 47 ++++++++++++--------------- internal/auth/copilot/errors.go | 5 +++ internal/auth/copilot/oauth.go | 4 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/auth/copilot/copilot_auth.go b/internal/auth/copilot/copilot_auth.go index e4e5876b..c40e7082 100644 --- a/internal/auth/copilot/copilot_auth.go +++ b/internal/auth/copilot/copilot_auth.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "strings" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -21,6 +20,13 @@ const ( copilotAPITokenURL = "https://api.github.com/copilot_internal/v2/token" // copilotAPIEndpoint is the base URL for making API requests. copilotAPIEndpoint = "https://api.githubcopilot.com" + + // Common HTTP header values for Copilot API requests. + copilotUserAgent = "GithubCopilot/1.0" + copilotEditorVersion = "vscode/1.100.0" + copilotPluginVersion = "copilot/1.300.0" + copilotIntegrationID = "vscode-chat" + copilotOpenAIIntent = "conversation-panel" ) // CopilotAPIToken represents the Copilot API token response. @@ -102,9 +108,9 @@ func (c *CopilotAuth) GetCopilotAPIToken(ctx context.Context, githubAccessToken req.Header.Set("Authorization", "token "+githubAccessToken) req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "GithubCopilot/1.0") - req.Header.Set("Editor-Version", "vscode/1.100.0") - req.Header.Set("Editor-Plugin-Version", "copilot/1.300.0") + req.Header.Set("User-Agent", copilotUserAgent) + req.Header.Set("Editor-Version", copilotEditorVersion) + req.Header.Set("Editor-Plugin-Version", copilotPluginVersion) resp, err := c.httpClient.Do(req) if err != nil { @@ -121,7 +127,7 @@ func (c *CopilotAuth) GetCopilotAPIToken(ctx context.Context, githubAccessToken return nil, NewAuthenticationError(ErrTokenExchangeFailed, err) } - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + if !isHTTPSuccess(resp.StatusCode) { return nil, NewAuthenticationError(ErrTokenExchangeFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes))) } @@ -199,32 +205,21 @@ func (c *CopilotAuth) MakeAuthenticatedRequest(ctx context.Context, method, url req.Header.Set("Authorization", "Bearer "+apiToken.Token) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "GithubCopilot/1.0") - req.Header.Set("Editor-Version", "vscode/1.100.0") - req.Header.Set("Editor-Plugin-Version", "copilot/1.300.0") - req.Header.Set("Openai-Intent", "conversation-panel") - req.Header.Set("Copilot-Integration-Id", "vscode-chat") + req.Header.Set("User-Agent", copilotUserAgent) + req.Header.Set("Editor-Version", copilotEditorVersion) + req.Header.Set("Editor-Plugin-Version", copilotPluginVersion) + req.Header.Set("Openai-Intent", copilotOpenAIIntent) + req.Header.Set("Copilot-Integration-Id", copilotIntegrationID) return req, nil } -// BuildChatCompletionURL builds the URL for chat completions API. -func BuildChatCompletionURL() string { +// buildChatCompletionURL builds the URL for chat completions API. +func buildChatCompletionURL() string { return copilotAPIEndpoint + "/chat/completions" } -// BuildModelsURL builds the URL for listing available models. -func BuildModelsURL() string { - return copilotAPIEndpoint + "/models" -} - -// ExtractBearerToken extracts the bearer token from an Authorization header. -func ExtractBearerToken(authHeader string) string { - if strings.HasPrefix(authHeader, "Bearer ") { - return strings.TrimPrefix(authHeader, "Bearer ") - } - if strings.HasPrefix(authHeader, "token ") { - return strings.TrimPrefix(authHeader, "token ") - } - return authHeader +// isHTTPSuccess checks if the status code indicates success (2xx). +func isHTTPSuccess(statusCode int) bool { + return statusCode >= 200 && statusCode < 300 } diff --git a/internal/auth/copilot/errors.go b/internal/auth/copilot/errors.go index dac6ecfa..a82dd8ec 100644 --- a/internal/auth/copilot/errors.go +++ b/internal/auth/copilot/errors.go @@ -55,6 +55,11 @@ func (e *AuthenticationError) Error() string { return fmt.Sprintf("%s: %s", e.Type, e.Message) } +// Unwrap returns the underlying cause of the error. +func (e *AuthenticationError) Unwrap() error { + return e.Cause +} + // Common authentication error types for GitHub Copilot device flow. var ( // ErrDeviceCodeFailed represents an error when requesting the device code fails. diff --git a/internal/auth/copilot/oauth.go b/internal/auth/copilot/oauth.go index 1aecf596..d3f46aaa 100644 --- a/internal/auth/copilot/oauth.go +++ b/internal/auth/copilot/oauth.go @@ -72,7 +72,7 @@ func (c *DeviceFlowClient) RequestDeviceCode(ctx context.Context) (*DeviceCodeRe } }() - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + if !isHTTPSuccess(resp.StatusCode) { bodyBytes, _ := io.ReadAll(resp.Body) return nil, NewAuthenticationError(ErrDeviceCodeFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes))) } @@ -235,7 +235,7 @@ func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string } }() - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + if !isHTTPSuccess(resp.StatusCode) { bodyBytes, _ := io.ReadAll(resp.Body) return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes))) }