refactor(auth): replace TokenRecord with coreauth.Auth and migrate TokenStore to coreauth.Store

- Replaced `TokenRecord` with `coreauth.Auth` for centralized and consistent authentication data structures.
- Migrated `TokenStore` interface to `coreauth.Store` for alignment with core CLIProxy authentication.
- Updated related login methods, token persistence logic, and file storage handling to use the new `coreauth.Auth` model.
This commit is contained in:
Luis Pater
2025-09-29 09:31:21 +08:00
parent 8cfa2282ef
commit d01c4904ff
17 changed files with 166 additions and 141 deletions

View File

@@ -160,11 +160,7 @@ func main() {
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok { if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir) dirSetter.SetBaseDir(cfg.AuthDir)
} }
store, ok := tokenStore.(coreauth.Store) core := coreauth.NewManager(tokenStore, nil, nil)
if !ok {
panic("token store does not implement coreauth.Store")
}
core := coreauth.NewManager(store, nil, nil)
core.RegisterExecutor(MyExecutor{}) core.RegisterExecutor(MyExecutor{})
hooks := cliproxy.Hooks{ hooks := cliproxy.Hooks{

View File

@@ -344,7 +344,7 @@ func (h *Handler) disableAuth(ctx context.Context, id string) {
} }
} }
func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenRecord) (string, error) { func (h *Handler) saveTokenRecord(ctx context.Context, record *coreauth.Auth) (string, error) {
if record == nil { if record == nil {
return "", fmt.Errorf("token record is nil") return "", fmt.Errorf("token record is nil")
} }
@@ -353,7 +353,12 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco
store = sdkAuth.GetTokenStore() store = sdkAuth.GetTokenStore()
h.tokenStore = store h.tokenStore = store
} }
return store.Save(ctx, h.cfg, record) if h.cfg != nil {
if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(h.cfg.AuthDir)
}
}
return store.Save(ctx, record)
} }
func (h *Handler) RequestAnthropicToken(c *gin.Context) { func (h *Handler) RequestAnthropicToken(c *gin.Context) {
@@ -496,11 +501,12 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
// Create token storage // Create token storage
tokenStorage := anthropicAuth.CreateTokenStorage(bundle) tokenStorage := anthropicAuth.CreateTokenStorage(bundle)
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
Provider: "claude", Provider: "claude",
FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email), FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
Storage: tokenStorage, Storage: tokenStorage,
Metadata: map[string]string{"email": tokenStorage.Email}, Metadata: map[string]any{"email": tokenStorage.Email},
} }
savedPath, errSave := h.saveTokenRecord(ctx, record) savedPath, errSave := h.saveTokenRecord(ctx, record)
if errSave != nil { if errSave != nil {
@@ -659,11 +665,12 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
} }
fmt.Println("Authentication successful.") fmt.Println("Authentication successful.")
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fmt.Sprintf("gemini-%s.json", ts.Email),
Provider: "gemini", Provider: "gemini",
FileName: fmt.Sprintf("gemini-%s.json", ts.Email), FileName: fmt.Sprintf("gemini-%s.json", ts.Email),
Storage: &ts, Storage: &ts,
Metadata: map[string]string{ Metadata: map[string]any{
"email": ts.Email, "email": ts.Email,
"project_id": ts.ProjectID, "project_id": ts.ProjectID,
}, },
@@ -724,7 +731,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
// Provide a stable label (gemini-web-<hash>) for logging and identification // Provide a stable label (gemini-web-<hash>) for logging and identification
tokenStorage.Label = strings.TrimSuffix(fileName, ".json") tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fileName,
Provider: "gemini-web", Provider: "gemini-web",
FileName: fileName, FileName: fileName,
Storage: tokenStorage, Storage: tokenStorage,
@@ -869,11 +877,12 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
// Create token storage and persist // Create token storage and persist
tokenStorage := openaiAuth.CreateTokenStorage(bundle) tokenStorage := openaiAuth.CreateTokenStorage(bundle)
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
Provider: "codex", Provider: "codex",
FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email), FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
Storage: tokenStorage, Storage: tokenStorage,
Metadata: map[string]string{ Metadata: map[string]any{
"email": tokenStorage.Email, "email": tokenStorage.Email,
"account_id": tokenStorage.AccountID, "account_id": tokenStorage.AccountID,
}, },
@@ -926,11 +935,12 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
tokenStorage := qwenAuth.CreateTokenStorage(tokenData) tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli()) tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
Provider: "qwen", Provider: "qwen",
FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email), FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
Storage: tokenStorage, Storage: tokenStorage,
Metadata: map[string]string{"email": tokenStorage.Email}, Metadata: map[string]any{"email": tokenStorage.Email},
} }
savedPath, errSave := h.saveTokenRecord(ctx, record) savedPath, errSave := h.saveTokenRecord(ctx, record)
if errSave != nil { if errSave != nil {

View File

@@ -33,7 +33,7 @@ type Handler struct {
failedAttempts map[string]*attemptInfo // keyed by client IP failedAttempts map[string]*attemptInfo // keyed by client IP
authManager *coreauth.Manager authManager *coreauth.Manager
usageStats *usage.RequestStatistics usageStats *usage.RequestStatistics
tokenStore sdkAuth.TokenStore tokenStore coreauth.Store
localPassword string localPassword string
} }

View File

@@ -18,6 +18,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
// banner prints a simple ASCII banner for clarity without ANSI colors. // banner prints a simple ASCII banner for clarity without ANSI colors.
@@ -173,13 +174,19 @@ func DoGeminiWebAuth(cfg *config.Config) {
Secure1PSIDTS: secure1psidts, Secure1PSIDTS: secure1psidts,
Label: label, Label: label,
} }
record := &sdkAuth.TokenRecord{ record := &coreauth.Auth{
ID: fileName,
Provider: "gemini-web", Provider: "gemini-web",
FileName: fileName, FileName: fileName,
Storage: tokenStorage, Storage: tokenStorage,
} }
store := sdkAuth.GetTokenStore() store := sdkAuth.GetTokenStore()
savedPath, err := store.Save(context.Background(), cfg, record) if cfg != nil {
if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir)
}
}
savedPath, err := store.Save(context.Background(), record)
if err != nil { if err != nil {
fmt.Println("!! Failed to save Gemini Web token to file:", err) fmt.Println("!! Failed to save Gemini Web token to file:", err)
return return

View File

@@ -13,6 +13,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -35,7 +36,7 @@ func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
return &d return &d
} }
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
if cfg == nil { if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required") return nil, fmt.Errorf("cliproxy auth: configuration is required")
} }
@@ -127,7 +128,7 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
} }
fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email) fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
metadata := map[string]string{ metadata := map[string]any{
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
@@ -136,7 +137,8 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
fmt.Println("Claude API key obtained and stored") fmt.Println("Claude API key obtained and stored")
} }
return &TokenRecord{ return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(), Provider: a.Provider(),
FileName: fileName, FileName: fileName,
Storage: tokenStorage, Storage: tokenStorage,

View File

@@ -13,6 +13,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -35,7 +36,7 @@ func (a *CodexAuthenticator) RefreshLead() *time.Duration {
return &d return &d
} }
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
if cfg == nil { if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required") return nil, fmt.Errorf("cliproxy auth: configuration is required")
} }
@@ -126,7 +127,7 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
} }
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email) fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
metadata := map[string]string{ metadata := map[string]any{
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
@@ -135,7 +136,8 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
fmt.Println("Codex API key obtained and stored") fmt.Println("Codex API key obtained and stored")
} }
return &TokenRecord{ return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(), Provider: a.Provider(),
FileName: fileName, FileName: fileName,
Storage: tokenStorage, Storage: tokenStorage,

View File

@@ -11,7 +11,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
@@ -35,27 +34,71 @@ func (s *FileTokenStore) SetBaseDir(dir string) {
s.dirLock.Unlock() s.dirLock.Unlock()
} }
// Save writes the token storage to the resolved file path. // Save persists token storage and metadata to the resolved auth file path.
func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) { func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) {
if record == nil || record.Storage == nil { if auth == nil {
return "", fmt.Errorf("cliproxy auth: token record is incomplete") return "", fmt.Errorf("auth filestore: auth is nil")
} }
target := strings.TrimSpace(record.FileName)
if target == "" { path, err := s.resolveAuthPath(auth)
return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider) if err != nil {
}
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 "", err
} }
return target, nil if path == "" {
return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID)
}
if auth.Disabled {
if _, statErr := os.Stat(path); 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)
}
switch {
case auth.Storage != nil:
if err = auth.Storage.SaveTokenToFile(path); err != nil {
return "", err
}
case auth.Metadata != nil:
raw, errMarshal := json.Marshal(auth.Metadata)
if errMarshal != nil {
return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal)
}
if existing, errRead := os.ReadFile(path); errRead == nil {
if jsonEqual(existing, raw) {
return path, nil
}
} else if errRead != nil && !os.IsNotExist(errRead) {
return "", fmt.Errorf("auth filestore: read existing failed: %w", errRead)
}
tmp := path + ".tmp"
if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil {
return "", fmt.Errorf("auth filestore: write temp failed: %w", errWrite)
}
if errRename := os.Rename(tmp, path); errRename != nil {
return "", fmt.Errorf("auth filestore: rename failed: %w", errRename)
}
default:
return "", fmt.Errorf("auth filestore: nothing to persist for %s", auth.ID)
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
auth.Attributes["path"] = path
if strings.TrimSpace(auth.FileName) == "" {
auth.FileName = auth.ID
}
return path, nil
} }
// List enumerates all auth JSON files under the configured directory. // List enumerates all auth JSON files under the configured directory.
@@ -90,50 +133,6 @@ func (s *FileTokenStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error)
return entries, nil 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. // Delete removes the auth file.
func (s *FileTokenStore) Delete(ctx context.Context, id string) error { func (s *FileTokenStore) Delete(ctx context.Context, id string) error {
id = strings.TrimSpace(id) id = strings.TrimSpace(id)
@@ -185,6 +184,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
auth := &cliproxyauth.Auth{ auth := &cliproxyauth.Auth{
ID: id, ID: id,
Provider: provider, Provider: provider,
FileName: id,
Label: s.labelFor(metadata), Label: s.labelFor(metadata),
Status: cliproxyauth.StatusActive, Status: cliproxyauth.StatusActive,
Attributes: map[string]string{"path": path}, Attributes: map[string]string{"path": path},
@@ -220,6 +220,15 @@ func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error
return p, nil return p, nil
} }
} }
if fileName := strings.TrimSpace(auth.FileName); fileName != "" {
if filepath.IsAbs(fileName) {
return fileName, nil
}
if dir := s.baseDirSnapshot(); dir != "" {
return filepath.Join(dir, fileName), nil
}
return fileName, nil
}
if auth.ID == "" { if auth.ID == "" {
return "", fmt.Errorf("auth filestore: missing id") return "", fmt.Errorf("auth filestore: missing id")
} }
@@ -249,13 +258,6 @@ func (s *FileTokenStore) labelFor(metadata map[string]any) string {
return "" 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 { func (s *FileTokenStore) baseDirSnapshot() string {
s.dirLock.RLock() s.dirLock.RLock()
defer s.dirLock.RUnlock() defer s.dirLock.RUnlock()

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
// GeminiWebAuthenticator provides a minimal wrapper so core components can treat // GeminiWebAuthenticator provides a minimal wrapper so core components can treat
@@ -16,7 +17,7 @@ func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuth
func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" } func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
_ = ctx _ = ctx
_ = cfg _ = cfg
_ = opts _ = opts

View File

@@ -8,6 +8,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
// legacy client removed // legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts. // GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
@@ -26,7 +27,7 @@ func (a *GeminiAuthenticator) RefreshLead() *time.Duration {
return nil return nil
} }
func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
if cfg == nil { if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required") return nil, fmt.Errorf("cliproxy auth: configuration is required")
} }
@@ -51,14 +52,15 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
// Skip onboarding here; rely on upstream configuration // Skip onboarding here; rely on upstream configuration
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID) fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
metadata := map[string]string{ metadata := map[string]any{
"email": ts.Email, "email": ts.Email,
"project_id": ts.ProjectID, "project_id": ts.ProjectID,
} }
fmt.Println("Gemini authentication successful") fmt.Println("Gemini authentication successful")
return &TokenRecord{ return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(), Provider: a.Provider(),
FileName: fileName, FileName: fileName,
Storage: &ts, Storage: &ts,

View File

@@ -5,8 +5,8 @@ import (
"errors" "errors"
"time" "time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported") var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
@@ -20,22 +20,9 @@ type LoginOptions struct {
Prompt func(prompt string) (string, error) 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. // Authenticator manages login and optional refresh flows for a provider.
type Authenticator interface { type Authenticator interface {
Provider() string Provider() string
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error)
RefreshLead() *time.Duration RefreshLead() *time.Duration
} }

View File

@@ -5,17 +5,18 @@ import (
"fmt" "fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
) )
// Manager aggregates authenticators and coordinates persistence via a token store. // Manager aggregates authenticators and coordinates persistence via a token store.
type Manager struct { type Manager struct {
authenticators map[string]Authenticator authenticators map[string]Authenticator
store TokenStore store coreauth.Store
} }
// NewManager constructs a manager with the provided token store and authenticators. // NewManager constructs a manager with the provided token store and authenticators.
// If store is nil, the caller must set it later using SetStore. // If store is nil, the caller must set it later using SetStore.
func NewManager(store TokenStore, authenticators ...Authenticator) *Manager { func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager {
mgr := &Manager{ mgr := &Manager{
authenticators: make(map[string]Authenticator), authenticators: make(map[string]Authenticator),
store: store, store: store,
@@ -38,12 +39,12 @@ func (m *Manager) Register(a Authenticator) {
} }
// SetStore updates the token store used for persistence. // SetStore updates the token store used for persistence.
func (m *Manager) SetStore(store TokenStore) { func (m *Manager) SetStore(store coreauth.Store) {
m.store = store m.store = store
} }
// Login executes the provider login flow and persists the resulting token record. // Login executes the provider login flow and persists the resulting auth record.
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) { func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) {
auth, ok := m.authenticators[provider] auth, ok := m.authenticators[provider]
if !ok { if !ok {
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider) return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
@@ -61,7 +62,13 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config
return record, "", nil return record, "", nil
} }
savedPath, err := m.store.Save(ctx, cfg, record) if cfg != nil {
if dirSetter, ok := m.store.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(cfg.AuthDir)
}
}
savedPath, err := m.store.Save(ctx, record)
if err != nil { if err != nil {
return record, "", err return record, "", err
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser" "github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
// legacy client removed // legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -30,7 +31,7 @@ func (a *QwenAuthenticator) RefreshLead() *time.Duration {
return &d return &d
} }
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
if cfg == nil { if cfg == nil {
return nil, fmt.Errorf("cliproxy auth: configuration is required") return nil, fmt.Errorf("cliproxy auth: configuration is required")
} }
@@ -97,13 +98,14 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
// no legacy client construction // no legacy client construction
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email) fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
metadata := map[string]string{ metadata := map[string]any{
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
fmt.Println("Qwen authentication successful") fmt.Println("Qwen authentication successful")
return &TokenRecord{ return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(), Provider: a.Provider(),
FileName: fileName, FileName: fileName,
Storage: tokenStorage, Storage: tokenStorage,

View File

@@ -1,31 +1,35 @@
package auth package auth
import "sync" import (
"sync"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)
var ( var (
storeMu sync.RWMutex storeMu sync.RWMutex
registeredTokenStore TokenStore registeredStore coreauth.Store
) )
// RegisterTokenStore sets the global token store used by the authentication helpers. // RegisterTokenStore sets the global token store used by the authentication helpers.
func RegisterTokenStore(store TokenStore) { func RegisterTokenStore(store coreauth.Store) {
storeMu.Lock() storeMu.Lock()
registeredTokenStore = store registeredStore = store
storeMu.Unlock() storeMu.Unlock()
} }
// GetTokenStore returns the globally registered token store. // GetTokenStore returns the globally registered token store.
func GetTokenStore() TokenStore { func GetTokenStore() coreauth.Store {
storeMu.RLock() storeMu.RLock()
s := registeredTokenStore s := registeredStore
storeMu.RUnlock() storeMu.RUnlock()
if s != nil { if s != nil {
return s return s
} }
storeMu.Lock() storeMu.Lock()
defer storeMu.Unlock() defer storeMu.Unlock()
if registeredTokenStore == nil { if registeredStore == nil {
registeredTokenStore = NewFileTokenStore() registeredStore = NewFileTokenStore()
} }
return registeredTokenStore return registeredStore
} }

View File

@@ -818,7 +818,8 @@ func (m *Manager) persist(ctx context.Context, auth *Auth) error {
if auth.Metadata == nil { if auth.Metadata == nil {
return nil return nil
} }
return m.store.SaveAuth(ctx, auth) _, err := m.store.Save(ctx, auth)
return err
} }
// StartAutoRefresh launches a background loop that evaluates auth freshness // StartAutoRefresh launches a background loop that evaluates auth freshness

View File

@@ -6,8 +6,8 @@ import "context"
type Store interface { type Store interface {
// List returns all auth records stored in the backend. // List returns all auth records stored in the backend.
List(ctx context.Context) ([]*Auth, error) List(ctx context.Context) ([]*Auth, error)
// SaveAuth persists the provided auth record, replacing any existing one with same ID. // Save persists the provided auth record, replacing any existing one with same ID.
SaveAuth(ctx context.Context, auth *Auth) error Save(ctx context.Context, auth *Auth) (string, error)
// Delete removes the auth record identified by id. // Delete removes the auth record identified by id.
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
} }

View File

@@ -6,6 +6,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
) )
// Auth encapsulates the runtime state and metadata associated with a single credential. // Auth encapsulates the runtime state and metadata associated with a single credential.
@@ -14,6 +16,10 @@ type Auth struct {
ID string `json:"id"` ID string `json:"id"`
// Provider is the upstream provider key (e.g. "gemini", "claude"). // Provider is the upstream provider key (e.g. "gemini", "claude").
Provider string `json:"provider"` Provider string `json:"provider"`
// FileName stores the relative or absolute path of the backing auth file.
FileName string `json:"-"`
// Storage holds the token persistence implementation used during login flows.
Storage baseauth.TokenStorage `json:"-"`
// Label is an optional human readable label for logging. // Label is an optional human readable label for logging.
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
// Status is the lifecycle status managed by the AuthManager. // Status is the lifecycle status managed by the AuthManager.

View File

@@ -197,11 +197,7 @@ func (b *Builder) Build() (*Service, error) {
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil { if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {
dirSetter.SetBaseDir(b.cfg.AuthDir) dirSetter.SetBaseDir(b.cfg.AuthDir)
} }
store, ok := tokenStore.(coreauth.Store) coreManager = coreauth.NewManager(tokenStore, nil, nil)
if !ok {
return nil, fmt.Errorf("cliproxy: token store does not implement coreauth.Store")
}
coreManager = coreauth.NewManager(store, nil, nil)
} }
// Attach a default RoundTripper provider so providers can opt-in per-auth transports. // Attach a default RoundTripper provider so providers can opt-in per-auth transports.
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider()) coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())