mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-26 03:06:11 +00:00
remove all
This commit is contained in:
@@ -1,145 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
// legacy client removed
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ClaudeAuthenticator implements the OAuth login flow for Anthropic Claude accounts.
|
||||
type ClaudeAuthenticator struct {
|
||||
CallbackPort int
|
||||
}
|
||||
|
||||
// NewClaudeAuthenticator constructs a Claude authenticator with default settings.
|
||||
func NewClaudeAuthenticator() *ClaudeAuthenticator {
|
||||
return &ClaudeAuthenticator{CallbackPort: 54545}
|
||||
}
|
||||
|
||||
func (a *ClaudeAuthenticator) Provider() string {
|
||||
return "claude"
|
||||
}
|
||||
|
||||
func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
|
||||
d := 4 * time.Hour
|
||||
return &d
|
||||
}
|
||||
|
||||
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
pkceCodes, err := claude.GeneratePKCECodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("claude pkce generation failed: %w", err)
|
||||
}
|
||||
|
||||
state, err := misc.GenerateRandomState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("claude state generation failed: %w", err)
|
||||
}
|
||||
|
||||
oauthServer := claude.NewOAuthServer(a.CallbackPort)
|
||||
if err = oauthServer.Start(); err != nil {
|
||||
if strings.Contains(err.Error(), "already in use") {
|
||||
return nil, claude.NewAuthenticationError(claude.ErrPortInUse, err)
|
||||
}
|
||||
return nil, claude.NewAuthenticationError(claude.ErrServerStartFailed, err)
|
||||
}
|
||||
defer func() {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
|
||||
log.Warnf("claude oauth server stop error: %v", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
authSvc := claude.NewClaudeAuth(cfg)
|
||||
|
||||
authURL, returnedState, err := authSvc.GenerateAuthURL(state, pkceCodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("claude authorization url generation failed: %w", err)
|
||||
}
|
||||
state = returnedState
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Claude authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
} else if err = browser.OpenURL(authURL); err != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
} else {
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
|
||||
log.Info("Waiting for Claude authentication callback...")
|
||||
|
||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "timeout") {
|
||||
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Error != "" {
|
||||
return nil, claude.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if result.State != state {
|
||||
return nil, claude.NewAuthenticationError(claude.ErrInvalidState, fmt.Errorf("state mismatch"))
|
||||
}
|
||||
|
||||
log.Debug("Claude authorization code received; exchanging for tokens")
|
||||
|
||||
authBundle, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, state, pkceCodes)
|
||||
if err != nil {
|
||||
return nil, claude.NewAuthenticationError(claude.ErrCodeExchangeFailed, err)
|
||||
}
|
||||
|
||||
tokenStorage := authSvc.CreateTokenStorage(authBundle)
|
||||
|
||||
if tokenStorage == nil || tokenStorage.Email == "" {
|
||||
return nil, fmt.Errorf("claude token storage missing account information")
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
|
||||
metadata := map[string]string{
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Claude authentication successful")
|
||||
if authBundle.APIKey != "" {
|
||||
log.Info("Claude API key obtained and stored")
|
||||
}
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Storage: tokenStorage,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
// legacy client removed
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// CodexAuthenticator implements the OAuth login flow for Codex accounts.
|
||||
type CodexAuthenticator struct {
|
||||
CallbackPort int
|
||||
}
|
||||
|
||||
// NewCodexAuthenticator constructs a Codex authenticator with default settings.
|
||||
func NewCodexAuthenticator() *CodexAuthenticator {
|
||||
return &CodexAuthenticator{CallbackPort: 1455}
|
||||
}
|
||||
|
||||
func (a *CodexAuthenticator) Provider() string {
|
||||
return "codex"
|
||||
}
|
||||
|
||||
func (a *CodexAuthenticator) RefreshLead() *time.Duration {
|
||||
d := 5 * 24 * time.Hour
|
||||
return &d
|
||||
}
|
||||
|
||||
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
pkceCodes, err := codex.GeneratePKCECodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("codex pkce generation failed: %w", err)
|
||||
}
|
||||
|
||||
state, err := misc.GenerateRandomState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("codex state generation failed: %w", err)
|
||||
}
|
||||
|
||||
oauthServer := codex.NewOAuthServer(a.CallbackPort)
|
||||
if err = oauthServer.Start(); err != nil {
|
||||
if strings.Contains(err.Error(), "already in use") {
|
||||
return nil, codex.NewAuthenticationError(codex.ErrPortInUse, err)
|
||||
}
|
||||
return nil, codex.NewAuthenticationError(codex.ErrServerStartFailed, err)
|
||||
}
|
||||
defer func() {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
|
||||
log.Warnf("codex oauth server stop error: %v", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
authSvc := codex.NewCodexAuth(cfg)
|
||||
|
||||
authURL, err := authSvc.GenerateAuthURL(state, pkceCodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("codex authorization url generation failed: %w", err)
|
||||
}
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Codex authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
} else if err = browser.OpenURL(authURL); err != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
} else {
|
||||
util.PrintSSHTunnelInstructions(a.CallbackPort)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
|
||||
log.Info("Waiting for Codex authentication callback...")
|
||||
|
||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "timeout") {
|
||||
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Error != "" {
|
||||
return nil, codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if result.State != state {
|
||||
return nil, codex.NewAuthenticationError(codex.ErrInvalidState, fmt.Errorf("state mismatch"))
|
||||
}
|
||||
|
||||
log.Debug("Codex authorization code received; exchanging for tokens")
|
||||
|
||||
authBundle, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, pkceCodes)
|
||||
if err != nil {
|
||||
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
|
||||
}
|
||||
|
||||
tokenStorage := authSvc.CreateTokenStorage(authBundle)
|
||||
|
||||
if tokenStorage == nil || tokenStorage.Email == "" {
|
||||
return nil, fmt.Errorf("codex token storage missing account information")
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
|
||||
metadata := map[string]string{
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Codex authentication successful")
|
||||
if authBundle.APIKey != "" {
|
||||
log.Info("Codex API key obtained and stored")
|
||||
}
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Storage: tokenStorage,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||
)
|
||||
|
||||
// ProjectSelectionError indicates that the user must choose a specific project ID.
|
||||
type ProjectSelectionError struct {
|
||||
Email string
|
||||
Projects []interfaces.GCPProjectProjects
|
||||
}
|
||||
|
||||
func (e *ProjectSelectionError) Error() string {
|
||||
if e == nil {
|
||||
return "cliproxy auth: project selection required"
|
||||
}
|
||||
return fmt.Sprintf("cliproxy auth: project selection required for %s", e.Email)
|
||||
}
|
||||
|
||||
// ProjectsDisplay returns the projects list for caller presentation.
|
||||
func (e *ProjectSelectionError) ProjectsDisplay() []interfaces.GCPProjectProjects {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Projects
|
||||
}
|
||||
|
||||
// EmailRequiredError indicates that the calling context must provide an email or alias.
|
||||
type EmailRequiredError struct {
|
||||
Prompt string
|
||||
}
|
||||
|
||||
func (e *EmailRequiredError) Error() string {
|
||||
if e == nil || e.Prompt == "" {
|
||||
return "cliproxy auth: email is required"
|
||||
}
|
||||
return e.Prompt
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
)
|
||||
|
||||
// FileTokenStore persists token records and auth metadata using the filesystem as backing storage.
|
||||
type FileTokenStore struct {
|
||||
mu sync.Mutex
|
||||
dirLock sync.RWMutex
|
||||
baseDir string
|
||||
}
|
||||
|
||||
// NewFileTokenStore creates a token store that saves credentials to disk through the
|
||||
// TokenStorage implementation embedded in the token record.
|
||||
func NewFileTokenStore() *FileTokenStore {
|
||||
return &FileTokenStore{}
|
||||
}
|
||||
|
||||
// SetBaseDir updates the default directory used for auth JSON persistence when no explicit path is provided.
|
||||
func (s *FileTokenStore) SetBaseDir(dir string) {
|
||||
s.dirLock.Lock()
|
||||
s.baseDir = strings.TrimSpace(dir)
|
||||
s.dirLock.Unlock()
|
||||
}
|
||||
|
||||
// Save writes the token storage to the resolved file path.
|
||||
func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) {
|
||||
if record == nil || record.Storage == nil {
|
||||
return "", fmt.Errorf("cliproxy auth: token record is incomplete")
|
||||
}
|
||||
target := strings.TrimSpace(record.FileName)
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider)
|
||||
}
|
||||
if !filepath.IsAbs(target) {
|
||||
baseDir := s.baseDirFromConfig(cfg)
|
||||
if baseDir != "" {
|
||||
target = filepath.Join(baseDir, target)
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := record.Storage.SaveTokenToFile(target); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// List enumerates all auth JSON files under the configured directory.
|
||||
func (s *FileTokenStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error) {
|
||||
dir := s.baseDirSnapshot()
|
||||
if dir == "" {
|
||||
return nil, fmt.Errorf("auth filestore: directory not configured")
|
||||
}
|
||||
entries := make([]*cliproxyauth.Auth, 0)
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(d.Name()), ".json") {
|
||||
return nil
|
||||
}
|
||||
auth, err := s.readAuthFile(path, dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if auth != nil {
|
||||
entries = append(entries, auth)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// SaveAuth writes the auth metadata back to its source file location.
|
||||
func (s *FileTokenStore) SaveAuth(ctx context.Context, auth *cliproxyauth.Auth) error {
|
||||
if auth == nil {
|
||||
return fmt.Errorf("auth filestore: auth is nil")
|
||||
}
|
||||
path, err := s.resolveAuthPath(auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path == "" {
|
||||
return fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID)
|
||||
}
|
||||
// If the auth has been disabled and the original file was removed, avoid recreating it on disk.
|
||||
if auth.Disabled {
|
||||
if _, statErr := os.Stat(path); statErr != nil {
|
||||
if os.IsNotExist(statErr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("auth filestore: create dir failed: %w", err)
|
||||
}
|
||||
raw, err := json.Marshal(auth.Metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth filestore: marshal metadata failed: %w", err)
|
||||
}
|
||||
if existing, errRead := os.ReadFile(path); errRead == nil {
|
||||
if jsonEqual(existing, raw) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err = os.WriteFile(tmp, raw, 0o600); err != nil {
|
||||
return fmt.Errorf("auth filestore: write temp failed: %w", err)
|
||||
}
|
||||
if err = os.Rename(tmp, path); err != nil {
|
||||
return fmt.Errorf("auth filestore: rename failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes the auth file.
|
||||
func (s *FileTokenStore) Delete(ctx context.Context, id string) error {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return fmt.Errorf("auth filestore: id is empty")
|
||||
}
|
||||
path, err := s.resolveDeletePath(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("auth filestore: delete failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) resolveDeletePath(id string) (string, error) {
|
||||
if strings.ContainsRune(id, os.PathSeparator) || filepath.IsAbs(id) {
|
||||
return id, nil
|
||||
}
|
||||
dir := s.baseDirSnapshot()
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("auth filestore: directory not configured")
|
||||
}
|
||||
return filepath.Join(dir, id), nil
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
metadata := make(map[string]any)
|
||||
if err = json.Unmarshal(data, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal auth json: %w", err)
|
||||
}
|
||||
provider, _ := metadata["type"].(string)
|
||||
if provider == "" {
|
||||
provider = "unknown"
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat file: %w", err)
|
||||
}
|
||||
id := s.idFor(path, baseDir)
|
||||
auth := &cliproxyauth.Auth{
|
||||
ID: id,
|
||||
Provider: provider,
|
||||
Label: s.labelFor(metadata),
|
||||
Status: cliproxyauth.StatusActive,
|
||||
Attributes: map[string]string{"path": path},
|
||||
Metadata: metadata,
|
||||
CreatedAt: info.ModTime(),
|
||||
UpdatedAt: info.ModTime(),
|
||||
LastRefreshedAt: time.Time{},
|
||||
NextRefreshAfter: time.Time{},
|
||||
}
|
||||
if email, ok := metadata["email"].(string); ok && email != "" {
|
||||
auth.Attributes["email"] = email
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) idFor(path, baseDir string) string {
|
||||
if baseDir == "" {
|
||||
return path
|
||||
}
|
||||
rel, err := filepath.Rel(baseDir, path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error) {
|
||||
if auth == nil {
|
||||
return "", fmt.Errorf("auth filestore: auth is nil")
|
||||
}
|
||||
if auth.Attributes != nil {
|
||||
if p := strings.TrimSpace(auth.Attributes["path"]); p != "" {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
if auth.ID == "" {
|
||||
return "", fmt.Errorf("auth filestore: missing id")
|
||||
}
|
||||
if filepath.IsAbs(auth.ID) {
|
||||
return auth.ID, nil
|
||||
}
|
||||
dir := s.baseDirSnapshot()
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("auth filestore: directory not configured")
|
||||
}
|
||||
return filepath.Join(dir, auth.ID), nil
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) labelFor(metadata map[string]any) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := metadata["label"].(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
if v, ok := metadata["email"].(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
if project, ok := metadata["project_id"].(string); ok && project != "" {
|
||||
return project
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) baseDirFromConfig(cfg *config.Config) string {
|
||||
if cfg != nil && strings.TrimSpace(cfg.AuthDir) != "" {
|
||||
return strings.TrimSpace(cfg.AuthDir)
|
||||
}
|
||||
return s.baseDirSnapshot()
|
||||
}
|
||||
|
||||
func (s *FileTokenStore) baseDirSnapshot() string {
|
||||
s.dirLock.RLock()
|
||||
defer s.dirLock.RUnlock()
|
||||
return s.baseDir
|
||||
}
|
||||
|
||||
func jsonEqual(a, b []byte) bool {
|
||||
var objA any
|
||||
var objB any
|
||||
if err := json.Unmarshal(a, &objA); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := json.Unmarshal(b, &objB); err != nil {
|
||||
return false
|
||||
}
|
||||
return deepEqualJSON(objA, objB)
|
||||
}
|
||||
|
||||
func deepEqualJSON(a, b any) bool {
|
||||
switch valA := a.(type) {
|
||||
case map[string]any:
|
||||
valB, ok := b.(map[string]any)
|
||||
if !ok || len(valA) != len(valB) {
|
||||
return false
|
||||
}
|
||||
for key, subA := range valA {
|
||||
subB, ok1 := valB[key]
|
||||
if !ok1 || !deepEqualJSON(subA, subB) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case []any:
|
||||
sliceB, ok := b.([]any)
|
||||
if !ok || len(valA) != len(sliceB) {
|
||||
return false
|
||||
}
|
||||
for i := range valA {
|
||||
if !deepEqualJSON(valA[i], sliceB[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case float64:
|
||||
valB, ok := b.(float64)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return valA == valB
|
||||
case string:
|
||||
valB, ok := b.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return valA == valB
|
||||
case bool:
|
||||
valB, ok := b.(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return valA == valB
|
||||
case nil:
|
||||
return b == nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
)
|
||||
|
||||
// GeminiWebAuthenticator provides a minimal wrapper so core components can treat
|
||||
// Gemini Web credentials via the shared Authenticator contract.
|
||||
type GeminiWebAuthenticator struct{}
|
||||
|
||||
func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuthenticator{} }
|
||||
|
||||
func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
|
||||
|
||||
func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
||||
_ = ctx
|
||||
_ = cfg
|
||||
_ = opts
|
||||
return nil, fmt.Errorf("gemini-web authenticator does not support scripted login; use CLI --gemini-web-auth")
|
||||
}
|
||||
|
||||
func (a *GeminiWebAuthenticator) RefreshLead() *time.Duration {
|
||||
d := 15 * time.Minute
|
||||
return &d
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||
// legacy client removed
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
|
||||
type GeminiAuthenticator struct{}
|
||||
|
||||
// NewGeminiAuthenticator constructs a Gemini authenticator.
|
||||
func NewGeminiAuthenticator() *GeminiAuthenticator {
|
||||
return &GeminiAuthenticator{}
|
||||
}
|
||||
|
||||
func (a *GeminiAuthenticator) Provider() string {
|
||||
return "gemini"
|
||||
}
|
||||
|
||||
func (a *GeminiAuthenticator) RefreshLead() *time.Duration {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
var ts gemini.GeminiTokenStorage
|
||||
if opts.ProjectID != "" {
|
||||
ts.ProjectID = opts.ProjectID
|
||||
}
|
||||
|
||||
geminiAuth := gemini.NewGeminiAuth()
|
||||
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, opts.NoBrowser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gemini authentication failed: %w", err)
|
||||
}
|
||||
|
||||
// Skip onboarding here; rely on upstream configuration
|
||||
|
||||
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
|
||||
metadata := map[string]string{
|
||||
"email": ts.Email,
|
||||
"project_id": ts.ProjectID,
|
||||
}
|
||||
|
||||
log.Info("Gemini authentication successful")
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Storage: &ts,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
)
|
||||
|
||||
var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
|
||||
|
||||
// LoginOptions captures generic knobs shared across authenticators.
|
||||
// Provider-specific logic can inspect Metadata for extra parameters.
|
||||
type LoginOptions struct {
|
||||
NoBrowser bool
|
||||
ProjectID string
|
||||
Metadata map[string]string
|
||||
Prompt func(prompt string) (string, error)
|
||||
}
|
||||
|
||||
// TokenRecord represents credential material produced by an authenticator.
|
||||
type TokenRecord struct {
|
||||
Provider string
|
||||
FileName string
|
||||
Storage baseauth.TokenStorage
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// TokenStore persists token records.
|
||||
type TokenStore interface {
|
||||
Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error)
|
||||
}
|
||||
|
||||
// Authenticator manages login and optional refresh flows for a provider.
|
||||
type Authenticator interface {
|
||||
Provider() string
|
||||
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error)
|
||||
RefreshLead() *time.Duration
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
)
|
||||
|
||||
// Manager aggregates authenticators and coordinates persistence via a token store.
|
||||
type Manager struct {
|
||||
authenticators map[string]Authenticator
|
||||
store TokenStore
|
||||
}
|
||||
|
||||
// NewManager constructs a manager with the provided token store and authenticators.
|
||||
// If store is nil, the caller must set it later using SetStore.
|
||||
func NewManager(store TokenStore, authenticators ...Authenticator) *Manager {
|
||||
mgr := &Manager{
|
||||
authenticators: make(map[string]Authenticator),
|
||||
store: store,
|
||||
}
|
||||
for i := range authenticators {
|
||||
mgr.Register(authenticators[i])
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
|
||||
// Register adds or replaces an authenticator keyed by its provider identifier.
|
||||
func (m *Manager) Register(a Authenticator) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
if m.authenticators == nil {
|
||||
m.authenticators = make(map[string]Authenticator)
|
||||
}
|
||||
m.authenticators[a.Provider()] = a
|
||||
}
|
||||
|
||||
// SetStore updates the token store used for persistence.
|
||||
func (m *Manager) SetStore(store TokenStore) {
|
||||
m.store = store
|
||||
}
|
||||
|
||||
// Login executes the provider login flow and persists the resulting token record.
|
||||
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) {
|
||||
auth, ok := m.authenticators[provider]
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
|
||||
}
|
||||
|
||||
record, err := auth.Login(ctx, cfg, opts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s returned nil record", provider)
|
||||
}
|
||||
|
||||
if m.store == nil {
|
||||
return record, "", nil
|
||||
}
|
||||
|
||||
savedPath, err := m.store.Save(ctx, cfg, record)
|
||||
if err != nil {
|
||||
return record, "", err
|
||||
}
|
||||
return record, savedPath, nil
|
||||
}
|
||||
112
sdk/auth/qwen.go
112
sdk/auth/qwen.go
@@ -1,112 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
// legacy client removed
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// QwenAuthenticator implements the device flow login for Qwen accounts.
|
||||
type QwenAuthenticator struct{}
|
||||
|
||||
// NewQwenAuthenticator constructs a Qwen authenticator.
|
||||
func NewQwenAuthenticator() *QwenAuthenticator {
|
||||
return &QwenAuthenticator{}
|
||||
}
|
||||
|
||||
func (a *QwenAuthenticator) Provider() string {
|
||||
return "qwen"
|
||||
}
|
||||
|
||||
func (a *QwenAuthenticator) RefreshLead() *time.Duration {
|
||||
d := 3 * time.Hour
|
||||
return &d
|
||||
}
|
||||
|
||||
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
authSvc := qwen.NewQwenAuth(cfg)
|
||||
|
||||
deviceFlow, err := authSvc.InitiateDeviceFlow(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("qwen device flow initiation failed: %w", err)
|
||||
}
|
||||
|
||||
authURL := deviceFlow.VerificationURIComplete
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Qwen authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
} else if err = browser.OpenURL(authURL); err != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
|
||||
}
|
||||
|
||||
log.Info("Waiting for Qwen authentication...")
|
||||
|
||||
tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("qwen authentication failed: %w", err)
|
||||
}
|
||||
|
||||
tokenStorage := authSvc.CreateTokenStorage(tokenData)
|
||||
|
||||
email := ""
|
||||
if opts.Metadata != nil {
|
||||
email = opts.Metadata["email"]
|
||||
if email == "" {
|
||||
email = opts.Metadata["alias"]
|
||||
}
|
||||
}
|
||||
|
||||
if email == "" && opts.Prompt != nil {
|
||||
email, err = opts.Prompt("Please input your email address or alias for Qwen:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return nil, &EmailRequiredError{Prompt: "Please provide an email address or alias for Qwen."}
|
||||
}
|
||||
|
||||
tokenStorage.Email = email
|
||||
|
||||
// no legacy client construction
|
||||
|
||||
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
|
||||
metadata := map[string]string{
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Qwen authentication successful")
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Storage: tokenStorage,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerRefreshLead("codex", func() Authenticator { return NewCodexAuthenticator() })
|
||||
registerRefreshLead("claude", func() Authenticator { return NewClaudeAuthenticator() })
|
||||
registerRefreshLead("qwen", func() Authenticator { return NewQwenAuthenticator() })
|
||||
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("gemini-web", func() Authenticator { return NewGeminiWebAuthenticator() })
|
||||
}
|
||||
|
||||
func registerRefreshLead(provider string, factory func() Authenticator) {
|
||||
cliproxyauth.RegisterRefreshLeadProvider(provider, func() *time.Duration {
|
||||
if factory == nil {
|
||||
return nil
|
||||
}
|
||||
auth := factory()
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
return auth.RefreshLead()
|
||||
})
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package auth
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
storeMu sync.RWMutex
|
||||
registeredTokenStore TokenStore
|
||||
)
|
||||
|
||||
// RegisterTokenStore sets the global token store used by the authentication helpers.
|
||||
func RegisterTokenStore(store TokenStore) {
|
||||
storeMu.Lock()
|
||||
registeredTokenStore = store
|
||||
storeMu.Unlock()
|
||||
}
|
||||
|
||||
// GetTokenStore returns the globally registered token store.
|
||||
func GetTokenStore() TokenStore {
|
||||
storeMu.RLock()
|
||||
s := registeredTokenStore
|
||||
storeMu.RUnlock()
|
||||
if s != nil {
|
||||
return s
|
||||
}
|
||||
storeMu.Lock()
|
||||
defer storeMu.Unlock()
|
||||
if registeredTokenStore == nil {
|
||||
registeredTokenStore = NewFileTokenStore()
|
||||
}
|
||||
return registeredTokenStore
|
||||
}
|
||||
Reference in New Issue
Block a user