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

@@ -13,6 +13,7 @@ import (
"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"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
@@ -35,7 +36,7 @@ func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
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 {
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)
metadata := map[string]string{
metadata := map[string]any{
"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")
}
return &TokenRecord{
return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(),
FileName: fileName,
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/misc"
"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"
)
@@ -35,7 +36,7 @@ func (a *CodexAuthenticator) RefreshLead() *time.Duration {
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 {
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)
metadata := map[string]string{
metadata := map[string]any{
"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")
}
return &TokenRecord{
return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(),
FileName: fileName,
Storage: tokenStorage,

View File

@@ -11,7 +11,6 @@ import (
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)
@@ -35,27 +34,71 @@ func (s *FileTokenStore) SetBaseDir(dir string) {
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")
// Save persists token storage and metadata to the resolved auth file path.
func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) {
if auth == nil {
return "", fmt.Errorf("auth filestore: auth is nil")
}
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 {
path, err := s.resolveAuthPath(auth)
if err != nil {
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.
@@ -90,50 +133,6 @@ func (s *FileTokenStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error)
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)
@@ -185,6 +184,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
auth := &cliproxyauth.Auth{
ID: id,
Provider: provider,
FileName: id,
Label: s.labelFor(metadata),
Status: cliproxyauth.StatusActive,
Attributes: map[string]string{"path": path},
@@ -220,6 +220,15 @@ func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error
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 == "" {
return "", fmt.Errorf("auth filestore: missing id")
}
@@ -249,13 +258,6 @@ func (s *FileTokenStore) labelFor(metadata map[string]any) string {
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()

View File

@@ -6,6 +6,7 @@ import (
"time"
"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
@@ -16,7 +17,7 @@ func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuth
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
_ = cfg
_ = opts

View File

@@ -8,6 +8,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
// legacy client removed
"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.
@@ -26,7 +27,7 @@ func (a *GeminiAuthenticator) RefreshLead() *time.Duration {
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 {
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
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
metadata := map[string]string{
metadata := map[string]any{
"email": ts.Email,
"project_id": ts.ProjectID,
}
fmt.Println("Gemini authentication successful")
return &TokenRecord{
return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(),
FileName: fileName,
Storage: &ts,

View File

@@ -5,8 +5,8 @@ import (
"errors"
"time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
"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")
@@ -20,22 +20,9 @@ type LoginOptions struct {
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)
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error)
RefreshLead() *time.Duration
}

View File

@@ -5,17 +5,18 @@ import (
"fmt"
"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.
type Manager struct {
authenticators map[string]Authenticator
store TokenStore
store coreauth.Store
}
// 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 {
func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager {
mgr := &Manager{
authenticators: make(map[string]Authenticator),
store: store,
@@ -38,12 +39,12 @@ func (m *Manager) Register(a Authenticator) {
}
// SetStore updates the token store used for persistence.
func (m *Manager) SetStore(store TokenStore) {
func (m *Manager) SetStore(store coreauth.Store) {
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) {
// 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) (*coreauth.Auth, string, error) {
auth, ok := m.authenticators[provider]
if !ok {
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
}
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 {
return record, "", err
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
// legacy client removed
"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"
)
@@ -30,7 +31,7 @@ func (a *QwenAuthenticator) RefreshLead() *time.Duration {
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 {
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
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
metadata := map[string]string{
metadata := map[string]any{
"email": tokenStorage.Email,
}
fmt.Println("Qwen authentication successful")
return &TokenRecord{
return &coreauth.Auth{
ID: fileName,
Provider: a.Provider(),
FileName: fileName,
Storage: tokenStorage,

View File

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

View File

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

View File

@@ -6,6 +6,8 @@ import (
"strings"
"sync"
"time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
)
// Auth encapsulates the runtime state and metadata associated with a single credential.
@@ -14,6 +16,10 @@ type Auth struct {
ID string `json:"id"`
// Provider is the upstream provider key (e.g. "gemini", "claude").
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 string `json:"label,omitempty"`
// 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 {
dirSetter.SetBaseDir(b.cfg.AuthDir)
}
store, ok := tokenStore.(coreauth.Store)
if !ok {
return nil, fmt.Errorf("cliproxy: token store does not implement coreauth.Store")
}
coreManager = coreauth.NewManager(store, nil, nil)
coreManager = coreauth.NewManager(tokenStore, nil, nil)
}
// Attach a default RoundTripper provider so providers can opt-in per-auth transports.
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())