package copilot import ( "errors" "fmt" "net/http" ) // OAuthError represents an OAuth-specific error. type OAuthError struct { // Code is the OAuth error code. Code string `json:"error"` // Description is a human-readable description of the error. Description string `json:"error_description,omitempty"` // URI is a URI identifying a human-readable web page with information about the error. URI string `json:"error_uri,omitempty"` // StatusCode is the HTTP status code associated with the error. StatusCode int `json:"-"` } // Error returns a string representation of the OAuth error. func (e *OAuthError) Error() string { if e.Description != "" { return fmt.Sprintf("OAuth error %s: %s", e.Code, e.Description) } return fmt.Sprintf("OAuth error: %s", e.Code) } // NewOAuthError creates a new OAuth error with the specified code, description, and status code. func NewOAuthError(code, description string, statusCode int) *OAuthError { return &OAuthError{ Code: code, Description: description, StatusCode: statusCode, } } // AuthenticationError represents authentication-related errors. type AuthenticationError struct { // Type is the type of authentication error. Type string `json:"type"` // Message is a human-readable message describing the error. Message string `json:"message"` // Code is the HTTP status code associated with the error. Code int `json:"code"` // Cause is the underlying error that caused this authentication error. Cause error `json:"-"` } // Error returns a string representation of the authentication error. func (e *AuthenticationError) Error() string { if e.Cause != nil { return fmt.Sprintf("%s: %s (caused by: %v)", e.Type, e.Message, e.Cause) } 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. ErrDeviceCodeFailed = &AuthenticationError{ Type: "device_code_failed", Message: "Failed to request device code from GitHub", Code: http.StatusBadRequest, } // ErrDeviceCodeExpired represents an error when the device code has expired. ErrDeviceCodeExpired = &AuthenticationError{ Type: "device_code_expired", Message: "Device code has expired. Please try again.", Code: http.StatusGone, } // ErrAuthorizationPending represents a pending authorization state (not an error, used for polling). ErrAuthorizationPending = &AuthenticationError{ Type: "authorization_pending", Message: "Authorization is pending. Waiting for user to authorize.", Code: http.StatusAccepted, } // ErrSlowDown represents a request to slow down polling. ErrSlowDown = &AuthenticationError{ Type: "slow_down", Message: "Polling too frequently. Slowing down.", Code: http.StatusTooManyRequests, } // ErrAccessDenied represents an error when the user denies authorization. ErrAccessDenied = &AuthenticationError{ Type: "access_denied", Message: "User denied authorization", Code: http.StatusForbidden, } // ErrTokenExchangeFailed represents an error when token exchange fails. ErrTokenExchangeFailed = &AuthenticationError{ Type: "token_exchange_failed", Message: "Failed to exchange device code for access token", Code: http.StatusBadRequest, } // ErrPollingTimeout represents an error when polling times out. ErrPollingTimeout = &AuthenticationError{ Type: "polling_timeout", Message: "Timeout waiting for user authorization", Code: http.StatusRequestTimeout, } // ErrUserInfoFailed represents an error when fetching user info fails. ErrUserInfoFailed = &AuthenticationError{ Type: "user_info_failed", Message: "Failed to fetch GitHub user information", Code: http.StatusBadRequest, } ) // NewAuthenticationError creates a new authentication error with a cause based on a base error. func NewAuthenticationError(baseErr *AuthenticationError, cause error) *AuthenticationError { return &AuthenticationError{ Type: baseErr.Type, Message: baseErr.Message, Code: baseErr.Code, Cause: cause, } } // IsAuthenticationError checks if an error is an authentication error. func IsAuthenticationError(err error) bool { var authenticationError *AuthenticationError ok := errors.As(err, &authenticationError) return ok } // IsOAuthError checks if an error is an OAuth error. func IsOAuthError(err error) bool { var oAuthError *OAuthError ok := errors.As(err, &oAuthError) return ok } // GetUserFriendlyMessage returns a user-friendly error message based on the error type. func GetUserFriendlyMessage(err error) string { var authErr *AuthenticationError if errors.As(err, &authErr) { switch authErr.Type { case "device_code_failed": return "Failed to start GitHub authentication. Please check your network connection and try again." case "device_code_expired": return "The authentication code has expired. Please try again." case "authorization_pending": return "Waiting for you to authorize the application on GitHub." case "slow_down": return "Please wait a moment before trying again." case "access_denied": return "Authentication was cancelled or denied." case "token_exchange_failed": return "Failed to complete authentication. Please try again." case "polling_timeout": return "Authentication timed out. Please try again." case "user_info_failed": return "Failed to get your GitHub account information. Please try again." default: return "Authentication failed. Please try again." } } var oauthErr *OAuthError if errors.As(err, &oauthErr) { switch oauthErr.Code { case "access_denied": return "Authentication was cancelled or denied." case "invalid_request": return "Invalid authentication request. Please try again." case "server_error": return "GitHub server error. Please try again later." default: return fmt.Sprintf("Authentication failed: %s", oauthErr.Description) } } return "An unexpected error occurred. Please try again." }