- 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.
Unified · Split
| @@ -160,11 +160,7 @@ func main() { · 159 unmodified lines | ||
| 160 | 160 | if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok { |
| 161 | 161 | dirSetter.SetBaseDir(cfg.AuthDir) |
| 162 | 162 | } |
| 163 | − store, ok := tokenStore.(coreauth.Store) | |
| 164 | − if !ok { | |
| 165 | − panic("token store does not implement coreauth.Store") | |
| 166 | − } | |
| 167 | − core := coreauth.NewManager(store, nil, nil) | |
| 163 | + core := coreauth.NewManager(tokenStore, nil, nil) | |
| 168 | 164 | core.RegisterExecutor(MyExecutor{}) |
| 169 | 165 | |
| 170 | 166 | hooks := cliproxy.Hooks{ |
| @@ -344,7 +344,7 @@ func (h *Handler) disableAuth(ctx context.Context, id string) { · 343 unmodified lines | ||
| 344 | 344 | } |
| 345 | 345 | } |
| 346 | 346 | |
| 347 | −func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenRecord) (string, error) { | |
| 347 | +func (h *Handler) saveTokenRecord(ctx context.Context, record *coreauth.Auth) (string, error) { | |
| 348 | 348 | if record == nil { |
| 349 | 349 | return "", fmt.Errorf("token record is nil") |
| 350 | 350 | } |
| @@ -353,7 +353,12 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco · 2 unmodified lines | ||
| 353 | 353 | store = sdkAuth.GetTokenStore() |
| 354 | 354 | h.tokenStore = store |
| 355 | 355 | } |
| 356 | − return store.Save(ctx, h.cfg, record) | |
| 356 | + if h.cfg != nil { | |
| 357 | + if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok { | |
| 358 | + dirSetter.SetBaseDir(h.cfg.AuthDir) | |
| 359 | + } | |
| 360 | + } | |
| 361 | + return store.Save(ctx, record) | |
| 357 | 362 | } |
| 358 | 363 | |
| 359 | 364 | func (h *Handler) RequestAnthropicToken(c *gin.Context) { |
| @@ -496,11 +501,12 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) { · 136 unmodified lines | ||
| 496 | 501 | |
| 497 | 502 | // Create token storage |
| 498 | 503 | tokenStorage := anthropicAuth.CreateTokenStorage(bundle) |
| 499 | − record := &sdkAuth.TokenRecord{ | |
| 504 | + record := &coreauth.Auth{ | |
| 505 | + ID: fmt.Sprintf("claude-%s.json", tokenStorage.Email), | |
| 500 | 506 | Provider: "claude", |
| 501 | 507 | FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email), |
| 502 | 508 | Storage: tokenStorage, |
| 503 | − Metadata: map[string]string{"email": tokenStorage.Email}, | |
| 509 | + Metadata: map[string]any{"email": tokenStorage.Email}, | |
| 504 | 510 | } |
| 505 | 511 | savedPath, errSave := h.saveTokenRecord(ctx, record) |
| 506 | 512 | if errSave != nil { |
| @@ -659,11 +665,12 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) { · 152 unmodified lines | ||
| 659 | 665 | } |
| 660 | 666 | fmt.Println("Authentication successful.") |
| 661 | 667 | |
| 662 | − record := &sdkAuth.TokenRecord{ | |
| 668 | + record := &coreauth.Auth{ | |
| 669 | + ID: fmt.Sprintf("gemini-%s.json", ts.Email), | |
| 663 | 670 | Provider: "gemini", |
| 664 | 671 | FileName: fmt.Sprintf("gemini-%s.json", ts.Email), |
| 665 | 672 | Storage: &ts, |
| 666 | − Metadata: map[string]string{ | |
| 673 | + Metadata: map[string]any{ | |
| 667 | 674 | "email": ts.Email, |
| 668 | 675 | "project_id": ts.ProjectID, |
| 669 | 676 | }, |
| @@ -724,7 +731,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { · 54 unmodified lines | ||
| 724 | 731 | // Provide a stable label (gemini-web-<hash>) for logging and identification |
| 725 | 732 | tokenStorage.Label = strings.TrimSuffix(fileName, ".json") |
| 726 | 733 | |
| 727 | − record := &sdkAuth.TokenRecord{ | |
| 734 | + record := &coreauth.Auth{ | |
| 735 | + ID: fileName, | |
| 728 | 736 | Provider: "gemini-web", |
| 729 | 737 | FileName: fileName, |
| 730 | 738 | Storage: tokenStorage, |
| @@ -869,11 +877,12 @@ func (h *Handler) RequestCodexToken(c *gin.Context) { · 138 unmodified lines | ||
| 869 | 877 | |
| 870 | 878 | // Create token storage and persist |
| 871 | 879 | tokenStorage := openaiAuth.CreateTokenStorage(bundle) |
| 872 | − record := &sdkAuth.TokenRecord{ | |
| 880 | + record := &coreauth.Auth{ | |
| 881 | + ID: fmt.Sprintf("codex-%s.json", tokenStorage.Email), | |
| 873 | 882 | Provider: "codex", |
| 874 | 883 | FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email), |
| 875 | 884 | Storage: tokenStorage, |
| 876 | − Metadata: map[string]string{ | |
| 885 | + Metadata: map[string]any{ | |
| 877 | 886 | "email": tokenStorage.Email, |
| 878 | 887 | "account_id": tokenStorage.AccountID, |
| 879 | 888 | }, |
| @@ -926,11 +935,12 @@ func (h *Handler) RequestQwenToken(c *gin.Context) { · 46 unmodified lines | ||
| 926 | 935 | tokenStorage := qwenAuth.CreateTokenStorage(tokenData) |
| 927 | 936 | |
| 928 | 937 | tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli()) |
| 929 | − record := &sdkAuth.TokenRecord{ | |
| 938 | + record := &coreauth.Auth{ | |
| 939 | + ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email), | |
| 930 | 940 | Provider: "qwen", |
| 931 | 941 | FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email), |
| 932 | 942 | Storage: tokenStorage, |
| 933 | − Metadata: map[string]string{"email": tokenStorage.Email}, | |
| 943 | + Metadata: map[string]any{"email": tokenStorage.Email}, | |
| 934 | 944 | } |
| 935 | 945 | savedPath, errSave := h.saveTokenRecord(ctx, record) |
| 936 | 946 | if errSave != nil { |
| @@ -33,7 +33,7 @@ type Handler struct { · 32 unmodified lines | ||
| 33 | 33 | failedAttempts map[string]*attemptInfo // keyed by client IP |
| 34 | 34 | authManager *coreauth.Manager |
| 35 | 35 | usageStats *usage.RequestStatistics |
| 36 | − tokenStore sdkAuth.TokenStore | |
| 36 | + tokenStore coreauth.Store | |
| 37 | 37 | |
| 38 | 38 | localPassword string |
| 39 | 39 | } |
| @@ -18,6 +18,7 @@ import ( · 17 unmodified lines | ||
| 18 | 18 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 19 | 19 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| 20 | 20 | sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" |
| 21 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 21 | 22 | ) |
| 22 | 23 | |
| 23 | 24 | // banner prints a simple ASCII banner for clarity without ANSI colors. |
| @@ -173,13 +174,19 @@ func DoGeminiWebAuth(cfg *config.Config) { · 149 unmodified lines | ||
| 173 | 174 | Secure1PSIDTS: secure1psidts, |
| 174 | 175 | Label: label, |
| 175 | 176 | } |
| 176 | − record := &sdkAuth.TokenRecord{ | |
| 177 | + record := &coreauth.Auth{ | |
| 178 | + ID: fileName, | |
| 177 | 179 | Provider: "gemini-web", |
| 178 | 180 | FileName: fileName, |
| 179 | 181 | Storage: tokenStorage, |
| 180 | 182 | } |
| 181 | 183 | store := sdkAuth.GetTokenStore() |
| 182 | − savedPath, err := store.Save(context.Background(), cfg, record) | |
| 184 | + if cfg != nil { | |
| 185 | + if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok { | |
| 186 | + dirSetter.SetBaseDir(cfg.AuthDir) | |
| 187 | + } | |
| 188 | + } | |
| 189 | + savedPath, err := store.Save(context.Background(), record) | |
| 183 | 190 | if err != nil { |
| 184 | 191 | fmt.Println("!! Failed to save Gemini Web token to file:", err) |
| 185 | 192 | return |
| @@ -13,6 +13,7 @@ import ( · 12 unmodified lines | ||
| 13 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 14 | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| 15 | 15 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| 16 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 16 | 17 | log "github.com/sirupsen/logrus" |
| 17 | 18 | ) |
| 18 | 19 | |
| @@ -35,7 +36,7 @@ func (a *ClaudeAuthenticator) RefreshLead() *time.Duration { · 16 unmodified lines | ||
| 35 | 36 | return &d |
| 36 | 37 | } |
| 37 | 38 | |
| 38 | −func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { | |
| 39 | +func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { | |
| 39 | 40 | if cfg == nil { |
| 40 | 41 | return nil, fmt.Errorf("cliproxy auth: configuration is required") |
| 41 | 42 | } |
| @@ -127,7 +128,7 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt · 85 unmodified lines | ||
| 127 | 128 | } |
| 128 | 129 | |
| 129 | 130 | fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email) |
| 130 | − metadata := map[string]string{ | |
| 131 | + metadata := map[string]any{ | |
| 131 | 132 | "email": tokenStorage.Email, |
| 132 | 133 | } |
| 133 | 134 | |
| @@ -136,7 +137,8 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt · 2 unmodified lines | ||
| 136 | 137 | fmt.Println("Claude API key obtained and stored") |
| 137 | 138 | } |
| 138 | 139 | |
| 139 | − return &TokenRecord{ | |
| 140 | + return &coreauth.Auth{ | |
| 141 | + ID: fileName, | |
| 140 | 142 | Provider: a.Provider(), |
| 141 | 143 | FileName: fileName, |
| 142 | 144 | Storage: tokenStorage, |
| @@ -13,6 +13,7 @@ import ( · 12 unmodified lines | ||
| 13 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 14 | 14 | "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" |
| 15 | 15 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| 16 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 16 | 17 | log "github.com/sirupsen/logrus" |
| 17 | 18 | ) |
| 18 | 19 | |
| @@ -35,7 +36,7 @@ func (a *CodexAuthenticator) RefreshLead() *time.Duration { · 16 unmodified lines | ||
| 35 | 36 | return &d |
| 36 | 37 | } |
| 37 | 38 | |
| 38 | −func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { | |
| 39 | +func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { | |
| 39 | 40 | if cfg == nil { |
| 40 | 41 | return nil, fmt.Errorf("cliproxy auth: configuration is required") |
| 41 | 42 | } |
| @@ -126,7 +127,7 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts · 84 unmodified lines | ||
| 126 | 127 | } |
| 127 | 128 | |
| 128 | 129 | fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email) |
| 129 | − metadata := map[string]string{ | |
| 130 | + metadata := map[string]any{ | |
| 130 | 131 | "email": tokenStorage.Email, |
| 131 | 132 | } |
| 132 | 133 | |
| @@ -135,7 +136,8 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts · 2 unmodified lines | ||
| 135 | 136 | fmt.Println("Codex API key obtained and stored") |
| 136 | 137 | } |
| 137 | 138 | |
| 138 | − return &TokenRecord{ | |
| 139 | + return &coreauth.Auth{ | |
| 140 | + ID: fileName, | |
| 139 | 141 | Provider: a.Provider(), |
| 140 | 142 | FileName: fileName, |
| 141 | 143 | Storage: tokenStorage, |
| @@ -11,7 +11,6 @@ import ( · 10 unmodified lines | ||
| 11 | 11 | "sync" |
| 12 | 12 | "time" |
| 13 | 13 | |
| 14 | − "github.com/router-for-me/CLIProxyAPI/v6/internal/config" | |
| 15 | 14 | cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| 16 | 15 | ) |
| 17 | 16 | |
| @@ -35,27 +34,71 @@ func (s *FileTokenStore) SetBaseDir(dir string) { · 17 unmodified lines | ||
| 35 | 34 | s.dirLock.Unlock() |
| 36 | 35 | } |
| 37 | 36 | |
| 38 | −// Save writes the token storage to the resolved file path. | |
| 39 | −func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) { | |
| 40 | − if record == nil || record.Storage == nil { | |
| 41 | − return "", fmt.Errorf("cliproxy auth: token record is incomplete") | |
| 42 | − } | |
| 43 | − target := strings.TrimSpace(record.FileName) | |
| 44 | − if target == "" { | |
| 45 | − return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider) | |
| 46 | − } | |
| 47 | − if !filepath.IsAbs(target) { | |
| 48 | − baseDir := s.baseDirFromConfig(cfg) | |
| 49 | − if baseDir != "" { | |
| 50 | − target = filepath.Join(baseDir, target) | |
| 37 | +// Save persists token storage and metadata to the resolved auth file path. | |
| 38 | +func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) { | |
| 39 | + if auth == nil { | |
| 40 | + return "", fmt.Errorf("auth filestore: auth is nil") | |
| 41 | + } | |
| 42 | + | |
| 43 | + path, err := s.resolveAuthPath(auth) | |
| 44 | + if err != nil { | |
| 45 | + return "", err | |
| 46 | + } | |
| 47 | + if path == "" { | |
| 48 | + return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID) | |
| 49 | + } | |
| 50 | + | |
| 51 | + if auth.Disabled { | |
| 52 | + if _, statErr := os.Stat(path); os.IsNotExist(statErr) { | |
| 53 | + return "", nil | |
| 51 | 54 | } |
| 52 | 55 | } |
| 56 | + | |
| 53 | 57 | s.mu.Lock() |
| 54 | 58 | defer s.mu.Unlock() |
| 55 | − if err := record.Storage.SaveTokenToFile(target); err != nil { | |
| 56 | − return "", err | |
| 59 | + | |
| 60 | + if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil { | |
| 61 | + return "", fmt.Errorf("auth filestore: create dir failed: %w", err) | |
| 57 | 62 | } |
| 58 | − return target, nil | |
| 63 | + | |
| 64 | + switch { | |
| 65 | + case auth.Storage != nil: | |
| 66 | + if err = auth.Storage.SaveTokenToFile(path); err != nil { | |
| 67 | + return "", err | |
| 68 | + } | |
| 69 | + case auth.Metadata != nil: | |
| 70 | + raw, errMarshal := json.Marshal(auth.Metadata) | |
| 71 | + if errMarshal != nil { | |
| 72 | + return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal) | |
| 73 | + } | |
| 74 | + if existing, errRead := os.ReadFile(path); errRead == nil { | |
| 75 | + if jsonEqual(existing, raw) { | |
| 76 | + return path, nil | |
| 77 | + } | |
| 78 | + } else if errRead != nil && !os.IsNotExist(errRead) { | |
| 79 | + return "", fmt.Errorf("auth filestore: read existing failed: %w", errRead) | |
| 80 | + } | |
| 81 | + tmp := path + ".tmp" | |
| 82 | + if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil { | |
| 83 | + return "", fmt.Errorf("auth filestore: write temp failed: %w", errWrite) | |
| 84 | + } | |
| 85 | + if errRename := os.Rename(tmp, path); errRename != nil { | |
| 86 | + return "", fmt.Errorf("auth filestore: rename failed: %w", errRename) | |
| 87 | + } | |
| 88 | + default: | |
| 89 | + return "", fmt.Errorf("auth filestore: nothing to persist for %s", auth.ID) | |
| 90 | + } | |
| 91 | + | |
| 92 | + if auth.Attributes == nil { | |
| 93 | + auth.Attributes = make(map[string]string) | |
| 94 | + } | |
| 95 | + auth.Attributes["path"] = path | |
| 96 | + | |
| 97 | + if strings.TrimSpace(auth.FileName) == "" { | |
| 98 | + auth.FileName = auth.ID | |
| 99 | + } | |
| 100 | + | |
| 101 | + return path, nil | |
| 59 | 102 | } |
| 60 | 103 | |
| 61 | 104 | // List enumerates all auth JSON files under the configured directory. |
| @@ -90,50 +133,6 @@ func (s *FileTokenStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error) · 28 unmodified lines | ||
| 90 | 133 | return entries, nil |
| 91 | 134 | } |
| 92 | 135 | |
| 93 | −// SaveAuth writes the auth metadata back to its source file location. | |
| 94 | −func (s *FileTokenStore) SaveAuth(ctx context.Context, auth *cliproxyauth.Auth) error { | |
| 95 | − if auth == nil { | |
| 96 | − return fmt.Errorf("auth filestore: auth is nil") | |
| 97 | − } | |
| 98 | − path, err := s.resolveAuthPath(auth) | |
| 99 | − if err != nil { | |
| 100 | − return err | |
| 101 | − } | |
| 102 | − if path == "" { | |
| 103 | − return fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID) | |
| 104 | − } | |
| 105 | − // If the auth has been disabled and the original file was removed, avoid recreating it on disk. | |
| 106 | − if auth.Disabled { | |
| 107 | − if _, statErr := os.Stat(path); statErr != nil { | |
| 108 | − if os.IsNotExist(statErr) { | |
| 109 | − return nil | |
| 110 | − } | |
| 111 | − } | |
| 112 | − } | |
| 113 | − s.mu.Lock() | |
| 114 | − defer s.mu.Unlock() | |
| 115 | − if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil { | |
| 116 | − return fmt.Errorf("auth filestore: create dir failed: %w", err) | |
| 117 | − } | |
| 118 | − raw, err := json.Marshal(auth.Metadata) | |
| 119 | − if err != nil { | |
| 120 | − return fmt.Errorf("auth filestore: marshal metadata failed: %w", err) | |
| 121 | − } | |
| 122 | − if existing, errRead := os.ReadFile(path); errRead == nil { | |
| 123 | − if jsonEqual(existing, raw) { | |
| 124 | − return nil | |
| 125 | − } | |
| 126 | − } | |
| 127 | − tmp := path + ".tmp" | |
| 128 | − if err = os.WriteFile(tmp, raw, 0o600); err != nil { | |
| 129 | − return fmt.Errorf("auth filestore: write temp failed: %w", err) | |
| 130 | − } | |
| 131 | − if err = os.Rename(tmp, path); err != nil { | |
| 132 | − return fmt.Errorf("auth filestore: rename failed: %w", err) | |
| 133 | − } | |
| 134 | − return nil | |
| 135 | −} | |
| 136 | − | |
| 137 | 136 | // Delete removes the auth file. |
| 138 | 137 | func (s *FileTokenStore) Delete(ctx context.Context, id string) error { |
| 139 | 138 | id = strings.TrimSpace(id) |
| @@ -185,6 +184,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, · 45 unmodified lines | ||
| 185 | 184 | auth := &cliproxyauth.Auth{ |
| 186 | 185 | ID: id, |
| 187 | 186 | Provider: provider, |
| 187 | + FileName: id, | |
| 188 | 188 | Label: s.labelFor(metadata), |
| 189 | 189 | Status: cliproxyauth.StatusActive, |
| 190 | 190 | Attributes: map[string]string{"path": path}, |
| @@ -220,6 +220,15 @@ func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error · 29 unmodified lines | ||
| 220 | 220 | return p, nil |
| 221 | 221 | } |
| 222 | 222 | } |
| 223 | + if fileName := strings.TrimSpace(auth.FileName); fileName != "" { | |
| 224 | + if filepath.IsAbs(fileName) { | |
| 225 | + return fileName, nil | |
| 226 | + } | |
| 227 | + if dir := s.baseDirSnapshot(); dir != "" { | |
| 228 | + return filepath.Join(dir, fileName), nil | |
| 229 | + } | |
| 230 | + return fileName, nil | |
| 231 | + } | |
| 223 | 232 | if auth.ID == "" { |
| 224 | 233 | return "", fmt.Errorf("auth filestore: missing id") |
| 225 | 234 | } |
| @@ -249,13 +258,6 @@ func (s *FileTokenStore) labelFor(metadata map[string]any) string { · 23 unmodified lines | ||
| 249 | 258 | return "" |
| 250 | 259 | } |
| 251 | 260 | |
| 252 | −func (s *FileTokenStore) baseDirFromConfig(cfg *config.Config) string { | |
| 253 | − if cfg != nil && strings.TrimSpace(cfg.AuthDir) != "" { | |
| 254 | − return strings.TrimSpace(cfg.AuthDir) | |
| 255 | − } | |
| 256 | − return s.baseDirSnapshot() | |
| 257 | −} | |
| 258 | − | |
| 259 | 261 | func (s *FileTokenStore) baseDirSnapshot() string { |
| 260 | 262 | s.dirLock.RLock() |
| 261 | 263 | defer s.dirLock.RUnlock() |
| @@ -6,6 +6,7 @@ import ( · 5 unmodified lines | ||
| 6 | 6 | "time" |
| 7 | 7 | |
| 8 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 9 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 9 | 10 | ) |
| 10 | 11 | |
| 11 | 12 | // GeminiWebAuthenticator provides a minimal wrapper so core components can treat |
| @@ -16,7 +17,7 @@ func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuth · 4 unmodified lines | ||
| 16 | 17 | |
| 17 | 18 | func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" } |
| 18 | 19 | |
| 19 | −func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { | |
| 20 | +func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { | |
| 20 | 21 | _ = ctx |
| 21 | 22 | _ = cfg |
| 22 | 23 | _ = opts |
| @@ -8,6 +8,7 @@ import ( · 7 unmodified lines | ||
| 8 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" |
| 9 | 9 | // legacy client removed |
| 10 | 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 11 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 11 | 12 | ) |
| 12 | 13 | |
| 13 | 14 | // GeminiAuthenticator implements the login flow for Google Gemini CLI accounts. |
| @@ -26,7 +27,7 @@ func (a *GeminiAuthenticator) RefreshLead() *time.Duration { · 12 unmodified lines | ||
| 26 | 27 | return nil |
| 27 | 28 | } |
| 28 | 29 | |
| 29 | −func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { | |
| 30 | +func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { | |
| 30 | 31 | if cfg == nil { |
| 31 | 32 | return nil, fmt.Errorf("cliproxy auth: configuration is required") |
| 32 | 33 | } |
| @@ -51,14 +52,15 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt · 18 unmodified lines | ||
| 51 | 52 | // Skip onboarding here; rely on upstream configuration |
| 52 | 53 | |
| 53 | 54 | fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID) |
| 54 | − metadata := map[string]string{ | |
| 55 | + metadata := map[string]any{ | |
| 55 | 56 | "email": ts.Email, |
| 56 | 57 | "project_id": ts.ProjectID, |
| 57 | 58 | } |
| 58 | 59 | |
| 59 | 60 | fmt.Println("Gemini authentication successful") |
| 60 | 61 | |
| 61 | − return &TokenRecord{ | |
| 62 | + return &coreauth.Auth{ | |
| 63 | + ID: fileName, | |
| 62 | 64 | Provider: a.Provider(), |
| 63 | 65 | FileName: fileName, |
| 64 | 66 | Storage: &ts, |
| @@ -5,8 +5,8 @@ import ( · 4 unmodified lines | ||
| 5 | 5 | "errors" |
| 6 | 6 | "time" |
| 7 | 7 | |
| 8 | − baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth" | |
| 9 | 8 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 9 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | 12 | var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported") |
| @@ -20,22 +20,9 @@ type LoginOptions struct { · 7 unmodified lines | ||
| 20 | 20 | Prompt func(prompt string) (string, error) |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | −// TokenRecord represents credential material produced by an authenticator. | |
| 24 | −type TokenRecord struct { | |
| 25 | − Provider string | |
| 26 | − FileName string | |
| 27 | − Storage baseauth.TokenStorage | |
| 28 | − Metadata map[string]string | |
| 29 | −} | |
| 30 | − | |
| 31 | −// TokenStore persists token records. | |
| 32 | −type TokenStore interface { | |
| 33 | − Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) | |
| 34 | −} | |
| 35 | − | |
| 36 | 23 | // Authenticator manages login and optional refresh flows for a provider. |
| 37 | 24 | type Authenticator interface { |
| 38 | 25 | Provider() string |
| 39 | − Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) | |
| 26 | + Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) | |
| 40 | 27 | RefreshLead() *time.Duration |
| 41 | 28 | } |
| @@ -5,17 +5,18 @@ import ( · 4 unmodified lines | ||
| 5 | 5 | "fmt" |
| 6 | 6 | |
| 7 | 7 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 8 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 8 | 9 | ) |
| 9 | 10 | |
| 10 | 11 | // Manager aggregates authenticators and coordinates persistence via a token store. |
| 11 | 12 | type Manager struct { |
| 12 | 13 | authenticators map[string]Authenticator |
| 13 | − store TokenStore | |
| 14 | + store coreauth.Store | |
| 14 | 15 | } |
| 15 | 16 | |
| 16 | 17 | // NewManager constructs a manager with the provided token store and authenticators. |
| 17 | 18 | // If store is nil, the caller must set it later using SetStore. |
| 18 | −func NewManager(store TokenStore, authenticators ...Authenticator) *Manager { | |
| 19 | +func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager { | |
| 19 | 20 | mgr := &Manager{ |
| 20 | 21 | authenticators: make(map[string]Authenticator), |
| 21 | 22 | store: store, |
| @@ -38,12 +39,12 @@ func (m *Manager) Register(a Authenticator) { · 16 unmodified lines | ||
| 38 | 39 | } |
| 39 | 40 | |
| 40 | 41 | // SetStore updates the token store used for persistence. |
| 41 | −func (m *Manager) SetStore(store TokenStore) { | |
| 42 | +func (m *Manager) SetStore(store coreauth.Store) { | |
| 42 | 43 | m.store = store |
| 43 | 44 | } |
| 44 | 45 | |
| 45 | −// Login executes the provider login flow and persists the resulting token record. | |
| 46 | −func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) { | |
| 46 | +// Login executes the provider login flow and persists the resulting auth record. | |
| 47 | +func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) { | |
| 47 | 48 | auth, ok := m.authenticators[provider] |
| 48 | 49 | if !ok { |
| 49 | 50 | 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 · 11 unmodified lines | ||
| 61 | 62 | return record, "", nil |
| 62 | 63 | } |
| 63 | 64 | |
| 64 | − savedPath, err := m.store.Save(ctx, cfg, record) | |
| 65 | + if cfg != nil { | |
| 66 | + if dirSetter, ok := m.store.(interface{ SetBaseDir(string) }); ok { | |
| 67 | + dirSetter.SetBaseDir(cfg.AuthDir) | |
| 68 | + } | |
| 69 | + } | |
| 70 | + | |
| 71 | + savedPath, err := m.store.Save(ctx, record) | |
| 65 | 72 | if err != nil { |
| 66 | 73 | return record, "", err |
| 67 | 74 | } |
| @@ -10,6 +10,7 @@ import ( · 9 unmodified lines | ||
| 10 | 10 | "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" |
| 11 | 11 | // legacy client removed |
| 12 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| 13 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 13 | 14 | log "github.com/sirupsen/logrus" |
| 14 | 15 | ) |
| 15 | 16 | |
| @@ -30,7 +31,7 @@ func (a *QwenAuthenticator) RefreshLead() *time.Duration { · 14 unmodified lines | ||
| 30 | 31 | return &d |
| 31 | 32 | } |
| 32 | 33 | |
| 33 | −func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) { | |
| 34 | +func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { | |
| 34 | 35 | if cfg == nil { |
| 35 | 36 | return nil, fmt.Errorf("cliproxy auth: configuration is required") |
| 36 | 37 | } |
| @@ -97,13 +98,14 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts · 60 unmodified lines | ||
| 97 | 98 | // no legacy client construction |
| 98 | 99 | |
| 99 | 100 | fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email) |
| 100 | − metadata := map[string]string{ | |
| 101 | + metadata := map[string]any{ | |
| 101 | 102 | "email": tokenStorage.Email, |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | 105 | fmt.Println("Qwen authentication successful") |
| 105 | 106 | |
| 106 | − return &TokenRecord{ | |
| 107 | + return &coreauth.Auth{ | |
| 108 | + ID: fileName, | |
| 107 | 109 | Provider: a.Provider(), |
| 108 | 110 | FileName: fileName, |
| 109 | 111 | Storage: tokenStorage, |
| @@ -1,31 +1,35 @@ | ||
| 1 | 1 | package auth |
| 2 | 2 | |
| 3 | −import "sync" | |
| 3 | +import ( | |
| 4 | + "sync" | |
| 5 | + | |
| 6 | + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| 7 | +) | |
| 4 | 8 | |
| 5 | 9 | var ( |
| 6 | − storeMu sync.RWMutex | |
| 7 | − registeredTokenStore TokenStore | |
| 10 | + storeMu sync.RWMutex | |
| 11 | + registeredStore coreauth.Store | |
| 8 | 12 | ) |
| 9 | 13 | |
| 10 | 14 | // RegisterTokenStore sets the global token store used by the authentication helpers. |
| 11 | −func RegisterTokenStore(store TokenStore) { | |
| 15 | +func RegisterTokenStore(store coreauth.Store) { | |
| 12 | 16 | storeMu.Lock() |
| 13 | − registeredTokenStore = store | |
| 17 | + registeredStore = store | |
| 14 | 18 | storeMu.Unlock() |
| 15 | 19 | } |
| 16 | 20 | |
| 17 | 21 | // GetTokenStore returns the globally registered token store. |
| 18 | −func GetTokenStore() TokenStore { | |
| 22 | +func GetTokenStore() coreauth.Store { | |
| 19 | 23 | storeMu.RLock() |
| 20 | − s := registeredTokenStore | |
| 24 | + s := registeredStore | |
| 21 | 25 | storeMu.RUnlock() |
| 22 | 26 | if s != nil { |
| 23 | 27 | return s |
| 24 | 28 | } |
| 25 | 29 | storeMu.Lock() |
| 26 | 30 | defer storeMu.Unlock() |
| 27 | − if registeredTokenStore == nil { | |
| 28 | − registeredTokenStore = NewFileTokenStore() | |
| 31 | + if registeredStore == nil { | |
| 32 | + registeredStore = NewFileTokenStore() | |
| 29 | 33 | } |
| 30 | − return registeredTokenStore | |
| 34 | + return registeredStore | |
| 31 | 35 | } |
| @@ -818,7 +818,8 @@ func (m *Manager) persist(ctx context.Context, auth *Auth) error { · 817 unmodified lines | ||
| 818 | 818 | if auth.Metadata == nil { |
| 819 | 819 | return nil |
| 820 | 820 | } |
| 821 | − return m.store.SaveAuth(ctx, auth) | |
| 821 | + _, err := m.store.Save(ctx, auth) | |
| 822 | + return err | |
| 822 | 823 | } |
| 823 | 824 | |
| 824 | 825 | // StartAutoRefresh launches a background loop that evaluates auth freshness |
| @@ -6,8 +6,8 @@ import "context" · 5 unmodified lines | ||
| 6 | 6 | type Store interface { |
| 7 | 7 | // List returns all auth records stored in the backend. |
| 8 | 8 | List(ctx context.Context) ([]*Auth, error) |
| 9 | − // SaveAuth persists the provided auth record, replacing any existing one with same ID. | |
| 10 | − SaveAuth(ctx context.Context, auth *Auth) error | |
| 9 | + // Save persists the provided auth record, replacing any existing one with same ID. | |
| 10 | + Save(ctx context.Context, auth *Auth) (string, error) | |
| 11 | 11 | // Delete removes the auth record identified by id. |
| 12 | 12 | Delete(ctx context.Context, id string) error |
| 13 | 13 | } |
| @@ -6,6 +6,8 @@ import ( · 5 unmodified lines | ||
| 6 | 6 | "strings" |
| 7 | 7 | "sync" |
| 8 | 8 | "time" |
| 9 | + | |
| 10 | + baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth" | |
| 9 | 11 | ) |
| 10 | 12 | |
| 11 | 13 | // Auth encapsulates the runtime state and metadata associated with a single credential. |
| @@ -14,6 +16,10 @@ type Auth struct { · 2 unmodified lines | ||
| 14 | 16 | ID string `json:"id"` |
| 15 | 17 | // Provider is the upstream provider key (e.g. "gemini", "claude"). |
| 16 | 18 | Provider string `json:"provider"` |
| 19 | + // FileName stores the relative or absolute path of the backing auth file. | |
| 20 | + FileName string `json:"-"` | |
| 21 | + // Storage holds the token persistence implementation used during login flows. | |
| 22 | + Storage baseauth.TokenStorage `json:"-"` | |
| 17 | 23 | // Label is an optional human readable label for logging. |
| 18 | 24 | Label string `json:"label,omitempty"` |
| 19 | 25 | // Status is the lifecycle status managed by the AuthManager. |
| @@ -197,11 +197,7 @@ func (b *Builder) Build() (*Service, error) { · 196 unmodified lines | ||
| 197 | 197 | if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil { |
| 198 | 198 | dirSetter.SetBaseDir(b.cfg.AuthDir) |
| 199 | 199 | } |
| 200 | − store, ok := tokenStore.(coreauth.Store) | |
| 201 | − if !ok { | |
| 202 | − return nil, fmt.Errorf("cliproxy: token store does not implement coreauth.Store") | |
| 203 | − } | |
| 204 | − coreManager = coreauth.NewManager(store, nil, nil) | |
| 200 | + coreManager = coreauth.NewManager(tokenStore, nil, nil) | |
| 205 | 201 | } |
| 206 | 202 | // Attach a default RoundTripper provider so providers can opt-in per-auth transports. |
| 207 | 203 | coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider()) |