Files
CLIProxyAPI/internal/auth/xai/token.go
T
Luis Pater e4c957078c feat(auth): add OAuth2 support for xAI with PKCE and token persistence
- Implemented xAI OAuth2 integration with PKCE (Proof Key for Code Exchange) support.
- Added logic for token exchange, refresh, and persistent storage in JSON format.
- Created `xai` package with helpers for OAuth discovery, API token handling, and URL building.
- Introduced `XAIExecutor` for integrating xAI credentials into runtime HTTP requests.
- Added unit tests to validate OAuth flow, token persistence, and endpoint validation.
2026-05-17 01:02:35 +08:00

105 lines
2.9 KiB
Go

package xai
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
log "github.com/sirupsen/logrus"
)
// TokenStorage stores xAI OAuth credentials on disk.
type TokenStorage struct {
Type string `json:"type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
Expire string `json:"expired,omitempty"`
LastRefresh string `json:"last_refresh,omitempty"`
Email string `json:"email,omitempty"`
Subject string `json:"sub,omitempty"`
BaseURL string `json:"base_url,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"`
TokenEndpoint string `json:"token_endpoint,omitempty"`
AuthKind string `json:"auth_kind,omitempty"`
Metadata map[string]any `json:"-"`
}
// SetMetadata allows the token store to merge status fields before saving.
func (ts *TokenStorage) SetMetadata(meta map[string]any) {
ts.Metadata = meta
}
// SaveTokenToFile writes xAI credentials to a JSON auth file.
func (ts *TokenStorage) SaveTokenToFile(authFilePath string) error {
misc.LogSavingCredentials(authFilePath)
ts.Type = "xai"
ts.AuthKind = "oauth"
if errMkdirAll := os.MkdirAll(filepath.Dir(authFilePath), 0o700); errMkdirAll != nil {
return fmt.Errorf("xai token storage: create directory: %w", errMkdirAll)
}
file, err := os.Create(authFilePath)
if err != nil {
return fmt.Errorf("xai token storage: create token file: %w", err)
}
defer func() {
if errClose := file.Close(); errClose != nil {
log.Errorf("xai token storage: close token file error: %v", errClose)
}
}()
data, errMerge := misc.MergeMetadata(ts, ts.Metadata)
if errMerge != nil {
return fmt.Errorf("xai token storage: merge metadata: %w", errMerge)
}
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err = encoder.Encode(data); err != nil {
return fmt.Errorf("xai token storage: write token file: %w", err)
}
return nil
}
// CredentialFileName returns the filename used for xAI credentials.
func CredentialFileName(email, subject string) string {
email = sanitizeFileSegment(email)
if email != "" {
return fmt.Sprintf("xai-%s.json", email)
}
subject = sanitizeFileSegment(subject)
if subject != "" {
return fmt.Sprintf("xai-%s.json", subject)
}
return fmt.Sprintf("xai-%d.json", time.Now().UnixMilli())
}
func sanitizeFileSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= 'A' && r <= 'Z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '@' || r == '.' || r == '_' || r == '-':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}