History

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.

Luis Pater committed 1 year agov6.0.10

Commit d01c4901 parent 8cfa22817 files · +164 −139Browse files

Unified · Split

17 files
  1. examples/custom-provider/main.go +1 −5
  2. internal/api/handlers/management/auth_files.go +21 −11
  3. internal/api/handlers/management/handler.go +1 −1
  4. internal/cmd/gemini-web_auth.go +9 −2
  5. sdk/auth/claude.go +5 −3
  6. sdk/auth/codex.go +5 −3
  7. sdk/auth/filestore.go +70 −68
  8. sdk/auth/gemini-web.go +2 −1
  9. sdk/auth/gemini.go +5 −3
  10. sdk/auth/interfaces.go +2 −15
  11. sdk/auth/manager.go +13 −6
  12. sdk/auth/qwen.go +5 −3
  13. sdk/auth/store_registry.go +14 −10
  14. sdk/cliproxy/auth/manager.go +2 −1
  15. sdk/cliproxy/auth/store.go +2 −2
  16. sdk/cliproxy/auth/types.go +6 −0
  17. sdk/cliproxy/builder.go +1 −5
examples/custom-provider/main.go+1−5
@@ -160,11 +160,7 @@ func main() { · 159 unmodified lines
160 if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {160 if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
161 dirSetter.SetBaseDir(cfg.AuthDir)161 dirSetter.SetBaseDir(cfg.AuthDir)
162 }162 }
163 store, ok := tokenStore.(coreauth.Store)163 core := coreauth.NewManager(tokenStore, nil, nil)
164 if !ok {
165 panic("token store does not implement coreauth.Store")
166 }
167 core := coreauth.NewManager(store, nil, nil)
168 core.RegisterExecutor(MyExecutor{})164 core.RegisterExecutor(MyExecutor{})
169 165
170 hooks := cliproxy.Hooks{166 hooks := cliproxy.Hooks{
internal/api/handlers/management/auth_files.go+21−11
@@ -344,7 +344,7 @@ func (h *Handler) disableAuth(ctx context.Context, id string) { · 343 unmodified lines
344 }344 }
345}345}
346 346
347func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenRecord) (string, error) {347func (h *Handler) saveTokenRecord(ctx context.Context, record *coreauth.Auth) (string, error) {
348 if record == nil {348 if record == nil {
349 return "", fmt.Errorf("token record is nil")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 store = sdkAuth.GetTokenStore()353 store = sdkAuth.GetTokenStore()
354 h.tokenStore = store354 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
359func (h *Handler) RequestAnthropicToken(c *gin.Context) {364func (h *Handler) RequestAnthropicToken(c *gin.Context) {
@@ -496,11 +501,12 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) { · 136 unmodified lines
496 501
497 // Create token storage502 // Create token storage
498 tokenStorage := anthropicAuth.CreateTokenStorage(bundle)503 tokenStorage := anthropicAuth.CreateTokenStorage(bundle)
499 record := &sdkAuth.TokenRecord{504 record := &coreauth.Auth{
505 ID: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
500 Provider: "claude",506 Provider: "claude",
501 FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email),507 FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
502 Storage: tokenStorage,508 Storage: tokenStorage,
503 Metadata: map[string]string{"email": tokenStorage.Email},509 Metadata: map[string]any{"email": tokenStorage.Email},
504 }510 }
505 savedPath, errSave := h.saveTokenRecord(ctx, record)511 savedPath, errSave := h.saveTokenRecord(ctx, record)
506 if errSave != nil {512 if errSave != nil {
@@ -659,11 +665,12 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) { · 152 unmodified lines
659 }665 }
660 fmt.Println("Authentication successful.")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 Provider: "gemini",670 Provider: "gemini",
664 FileName: fmt.Sprintf("gemini-%s.json", ts.Email),671 FileName: fmt.Sprintf("gemini-%s.json", ts.Email),
665 Storage: &ts,672 Storage: &ts,
666 Metadata: map[string]string{673 Metadata: map[string]any{
667 "email": ts.Email,674 "email": ts.Email,
668 "project_id": ts.ProjectID,675 "project_id": ts.ProjectID,
669 },676 },
@@ -724,7 +731,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { · 54 unmodified lines
724 // Provide a stable label (gemini-web-<hash>) for logging and identification731 // Provide a stable label (gemini-web-<hash>) for logging and identification
725 tokenStorage.Label = strings.TrimSuffix(fileName, ".json")732 tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
726 733
727 record := &sdkAuth.TokenRecord{734 record := &coreauth.Auth{
735 ID: fileName,
728 Provider: "gemini-web",736 Provider: "gemini-web",
729 FileName: fileName,737 FileName: fileName,
730 Storage: tokenStorage,738 Storage: tokenStorage,
@@ -869,11 +877,12 @@ func (h *Handler) RequestCodexToken(c *gin.Context) { · 138 unmodified lines
869 877
870 // Create token storage and persist878 // Create token storage and persist
871 tokenStorage := openaiAuth.CreateTokenStorage(bundle)879 tokenStorage := openaiAuth.CreateTokenStorage(bundle)
872 record := &sdkAuth.TokenRecord{880 record := &coreauth.Auth{
881 ID: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
873 Provider: "codex",882 Provider: "codex",
874 FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email),883 FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
875 Storage: tokenStorage,884 Storage: tokenStorage,
876 Metadata: map[string]string{885 Metadata: map[string]any{
877 "email": tokenStorage.Email,886 "email": tokenStorage.Email,
878 "account_id": tokenStorage.AccountID,887 "account_id": tokenStorage.AccountID,
879 },888 },
@@ -926,11 +935,12 @@ func (h *Handler) RequestQwenToken(c *gin.Context) { · 46 unmodified lines
926 tokenStorage := qwenAuth.CreateTokenStorage(tokenData)935 tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
927 936
928 tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())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 Provider: "qwen",940 Provider: "qwen",
931 FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),941 FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
932 Storage: tokenStorage,942 Storage: tokenStorage,
933 Metadata: map[string]string{"email": tokenStorage.Email},943 Metadata: map[string]any{"email": tokenStorage.Email},
934 }944 }
935 savedPath, errSave := h.saveTokenRecord(ctx, record)945 savedPath, errSave := h.saveTokenRecord(ctx, record)
936 if errSave != nil {946 if errSave != nil {
internal/api/handlers/management/handler.go+1−1
@@ -33,7 +33,7 @@ type Handler struct { · 32 unmodified lines
33 failedAttempts map[string]*attemptInfo // keyed by client IP33 failedAttempts map[string]*attemptInfo // keyed by client IP
34 authManager *coreauth.Manager34 authManager *coreauth.Manager
35 usageStats *usage.RequestStatistics35 usageStats *usage.RequestStatistics
36 tokenStore sdkAuth.TokenStore36 tokenStore coreauth.Store
37 37
38 localPassword string38 localPassword string
39}39}
internal/cmd/gemini-web_auth.go+9−2
@@ -18,6 +18,7 @@ import ( · 17 unmodified lines
18 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"18 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
19 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"19 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
20 sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"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// banner prints a simple ASCII banner for clarity without ANSI colors.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 Secure1PSIDTS: secure1psidts,174 Secure1PSIDTS: secure1psidts,
174 Label: label,175 Label: label,
175 }176 }
176 record := &sdkAuth.TokenRecord{177 record := &coreauth.Auth{
178 ID: fileName,
177 Provider: "gemini-web",179 Provider: "gemini-web",
178 FileName: fileName,180 FileName: fileName,
179 Storage: tokenStorage,181 Storage: tokenStorage,
180 }182 }
181 store := sdkAuth.GetTokenStore()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 if err != nil {190 if err != nil {
184 fmt.Println("!! Failed to save Gemini Web token to file:", err)191 fmt.Println("!! Failed to save Gemini Web token to file:", err)
185 return192 return
sdk/auth/claude.go+5−3
@@ -13,6 +13,7 @@ import ( · 12 unmodified lines
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"13 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"14 "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
16 coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
16 log "github.com/sirupsen/logrus"17 log "github.com/sirupsen/logrus"
17)18)
18 19
@@ -35,7 +36,7 @@ func (a *ClaudeAuthenticator) RefreshLead() *time.Duration { · 16 unmodified lines
35 return &d36 return &d
36}37}
37 38
38func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {39func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
39 if cfg == nil {40 if cfg == nil {
40 return nil, fmt.Errorf("cliproxy auth: configuration is required")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 fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)130 fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
130 metadata := map[string]string{131 metadata := map[string]any{
131 "email": tokenStorage.Email,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 fmt.Println("Claude API key obtained and stored")137 fmt.Println("Claude API key obtained and stored")
137 }138 }
138 139
139 return &TokenRecord{140 return &coreauth.Auth{
141 ID: fileName,
140 Provider: a.Provider(),142 Provider: a.Provider(),
141 FileName: fileName,143 FileName: fileName,
142 Storage: tokenStorage,144 Storage: tokenStorage,
sdk/auth/codex.go+5−3
@@ -13,6 +13,7 @@ import ( · 12 unmodified lines
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"13 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"14 "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
16 coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
16 log "github.com/sirupsen/logrus"17 log "github.com/sirupsen/logrus"
17)18)
18 19
@@ -35,7 +36,7 @@ func (a *CodexAuthenticator) RefreshLead() *time.Duration { · 16 unmodified lines
35 return &d36 return &d
36}37}
37 38
38func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {39func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
39 if cfg == nil {40 if cfg == nil {
40 return nil, fmt.Errorf("cliproxy auth: configuration is required")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 fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)129 fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
129 metadata := map[string]string{130 metadata := map[string]any{
130 "email": tokenStorage.Email,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 fmt.Println("Codex API key obtained and stored")136 fmt.Println("Codex API key obtained and stored")
136 }137 }
137 138
138 return &TokenRecord{139 return &coreauth.Auth{
140 ID: fileName,
139 Provider: a.Provider(),141 Provider: a.Provider(),
140 FileName: fileName,142 FileName: fileName,
141 Storage: tokenStorage,143 Storage: tokenStorage,
sdk/auth/filestore.go+70−68
@@ -11,7 +11,6 @@ import ( · 10 unmodified lines
11 "sync"11 "sync"
12 "time"12 "time"
13 13
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
15 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"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 s.dirLock.Unlock()34 s.dirLock.Unlock()
36}35}
37 36
38// Save writes the token storage to the resolved file path.37// Save persists token storage and metadata to the resolved auth file path.
39func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) {38func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) {
40 if record == nil || record.Storage == nil {39 if auth == nil {
41 return "", fmt.Errorf("cliproxy auth: token record is incomplete")40 return "", fmt.Errorf("auth filestore: auth is nil")
42 }41 }
43 target := strings.TrimSpace(record.FileName)42
44 if target == "" {43 path, err := s.resolveAuthPath(auth)
45 return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider)44 if err != nil {
46 }45 return "", err
47 if !filepath.IsAbs(target) {46 }
48 baseDir := s.baseDirFromConfig(cfg)47 if path == "" {
49 if baseDir != "" {48 return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID)
50 target = filepath.Join(baseDir, target)49 }
50
51 if auth.Disabled {
52 if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
53 return "", nil
51 }54 }
52 }55 }
56
53 s.mu.Lock()57 s.mu.Lock()
54 defer s.mu.Unlock()58 defer s.mu.Unlock()
55 if err := record.Storage.SaveTokenToFile(target); err != nil {59
56 return "", err60 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, nil63
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// List enumerates all auth JSON files under the configured directory.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 return entries, nil133 return entries, nil
91}134}
92 135
93// SaveAuth writes the auth metadata back to its source file location.
94func (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// Delete removes the auth file.136// Delete removes the auth file.
138func (s *FileTokenStore) Delete(ctx context.Context, id string) error {137func (s *FileTokenStore) Delete(ctx context.Context, id string) error {
139 id = strings.TrimSpace(id)138 id = strings.TrimSpace(id)
@@ -185,6 +184,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, · 45 unmodified lines
185 auth := &cliproxyauth.Auth{184 auth := &cliproxyauth.Auth{
186 ID: id,185 ID: id,
187 Provider: provider,186 Provider: provider,
187 FileName: id,
188 Label: s.labelFor(metadata),188 Label: s.labelFor(metadata),
189 Status: cliproxyauth.StatusActive,189 Status: cliproxyauth.StatusActive,
190 Attributes: map[string]string{"path": path},190 Attributes: map[string]string{"path": path},
@@ -220,6 +220,15 @@ func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error · 29 unmodified lines
220 return p, nil220 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 if auth.ID == "" {232 if auth.ID == "" {
224 return "", fmt.Errorf("auth filestore: missing id")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 return ""258 return ""
250}259}
251 260
252func (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
259func (s *FileTokenStore) baseDirSnapshot() string {261func (s *FileTokenStore) baseDirSnapshot() string {
260 s.dirLock.RLock()262 s.dirLock.RLock()
261 defer s.dirLock.RUnlock()263 defer s.dirLock.RUnlock()
sdk/auth/gemini-web.go+2−1
@@ -6,6 +6,7 @@ import ( · 5 unmodified lines
6 "time"6 "time"
7 7
8 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"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// GeminiWebAuthenticator provides a minimal wrapper so core components can treat12// GeminiWebAuthenticator provides a minimal wrapper so core components can treat
@@ -16,7 +17,7 @@ func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuth · 4 unmodified lines
16 17
17func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }18func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
18 19
19func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {20func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
20 _ = ctx21 _ = ctx
21 _ = cfg22 _ = cfg
22 _ = opts23 _ = opts
sdk/auth/gemini.go+5−3
@@ -8,6 +8,7 @@ import ( · 7 unmodified lines
8 "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"8 "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
9 // legacy client removed9 // legacy client removed
10 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"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// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.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 return nil27 return nil
27}28}
28 29
29func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {30func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
30 if cfg == nil {31 if cfg == nil {
31 return nil, fmt.Errorf("cliproxy auth: configuration is required")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 // Skip onboarding here; rely on upstream configuration52 // Skip onboarding here; rely on upstream configuration
52 53
53 fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)54 fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
54 metadata := map[string]string{55 metadata := map[string]any{
55 "email": ts.Email,56 "email": ts.Email,
56 "project_id": ts.ProjectID,57 "project_id": ts.ProjectID,
57 }58 }
58 59
59 fmt.Println("Gemini authentication successful")60 fmt.Println("Gemini authentication successful")
60 61
61 return &TokenRecord{62 return &coreauth.Auth{
63 ID: fileName,
62 Provider: a.Provider(),64 Provider: a.Provider(),
63 FileName: fileName,65 FileName: fileName,
64 Storage: &ts,66 Storage: &ts,
sdk/auth/interfaces.go+2−15
@@ -5,8 +5,8 @@ import ( · 4 unmodified lines
5 "errors"5 "errors"
6 "time"6 "time"
7 7
8 baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
9 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"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
12var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")12var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
@@ -20,22 +20,9 @@ type LoginOptions struct { · 7 unmodified lines
20 Prompt func(prompt string) (string, error)20 Prompt func(prompt string) (string, error)
21}21}
22 22
23// TokenRecord represents credential material produced by an authenticator.
24type TokenRecord struct {
25 Provider string
26 FileName string
27 Storage baseauth.TokenStorage
28 Metadata map[string]string
29}
30
31// TokenStore persists token records.
32type TokenStore interface {
33 Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error)
34}
35
36// Authenticator manages login and optional refresh flows for a provider.23// Authenticator manages login and optional refresh flows for a provider.
37type Authenticator interface {24type Authenticator interface {
38 Provider() string25 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 RefreshLead() *time.Duration27 RefreshLead() *time.Duration
41}28}
sdk/auth/manager.go+13−6
@@ -5,17 +5,18 @@ import ( · 4 unmodified lines
5 "fmt"5 "fmt"
6 6
7 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"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// Manager aggregates authenticators and coordinates persistence via a token store.11// Manager aggregates authenticators and coordinates persistence via a token store.
11type Manager struct {12type Manager struct {
12 authenticators map[string]Authenticator13 authenticators map[string]Authenticator
13 store TokenStore14 store coreauth.Store
14}15}
15 16
16// NewManager constructs a manager with the provided token store and authenticators.17// NewManager constructs a manager with the provided token store and authenticators.
17// If store is nil, the caller must set it later using SetStore.18// If store is nil, the caller must set it later using SetStore.
18func NewManager(store TokenStore, authenticators ...Authenticator) *Manager {19func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager {
19 mgr := &Manager{20 mgr := &Manager{
20 authenticators: make(map[string]Authenticator),21 authenticators: make(map[string]Authenticator),
21 store: store,22 store: store,
@@ -38,12 +39,12 @@ func (m *Manager) Register(a Authenticator) { · 16 unmodified lines
38}39}
39 40
40// SetStore updates the token store used for persistence.41// SetStore updates the token store used for persistence.
41func (m *Manager) SetStore(store TokenStore) {42func (m *Manager) SetStore(store coreauth.Store) {
42 m.store = store43 m.store = store
43}44}
44 45
45// Login executes the provider login flow and persists the resulting token record.46// Login executes the provider login flow and persists the resulting auth record.
46func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) {47func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) {
47 auth, ok := m.authenticators[provider]48 auth, ok := m.authenticators[provider]
48 if !ok {49 if !ok {
49 return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)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 return record, "", nil62 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 if err != nil {72 if err != nil {
66 return record, "", err73 return record, "", err
67 }74 }
sdk/auth/qwen.go+5−3
@@ -10,6 +10,7 @@ import ( · 9 unmodified lines
10 "github.com/router-for-me/CLIProxyAPI/v6/internal/browser"10 "github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
11 // legacy client removed11 // legacy client removed
12 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"12 "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
13 coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
13 log "github.com/sirupsen/logrus"14 log "github.com/sirupsen/logrus"
14)15)
15 16
@@ -30,7 +31,7 @@ func (a *QwenAuthenticator) RefreshLead() *time.Duration { · 14 unmodified lines
30 return &d31 return &d
31}32}
32 33
33func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {34func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
34 if cfg == nil {35 if cfg == nil {
35 return nil, fmt.Errorf("cliproxy auth: configuration is required")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 // no legacy client construction98 // no legacy client construction
98 99
99 fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)100 fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
100 metadata := map[string]string{101 metadata := map[string]any{
101 "email": tokenStorage.Email,102 "email": tokenStorage.Email,
102 }103 }
103 104
104 fmt.Println("Qwen authentication successful")105 fmt.Println("Qwen authentication successful")
105 106
106 return &TokenRecord{107 return &coreauth.Auth{
108 ID: fileName,
107 Provider: a.Provider(),109 Provider: a.Provider(),
108 FileName: fileName,110 FileName: fileName,
109 Storage: tokenStorage,111 Storage: tokenStorage,
sdk/auth/store_registry.go+14−10
@@ -1,31 +1,35 @@
1package auth1package auth
2 2
3import "sync"3import (
4 "sync"
5
6 coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
7)
4 8
5var (9var (
6 storeMu sync.RWMutex10 storeMu sync.RWMutex
7 registeredTokenStore TokenStore11 registeredStore coreauth.Store
8)12)
9 13
10// RegisterTokenStore sets the global token store used by the authentication helpers.14// RegisterTokenStore sets the global token store used by the authentication helpers.
11func RegisterTokenStore(store TokenStore) {15func RegisterTokenStore(store coreauth.Store) {
12 storeMu.Lock()16 storeMu.Lock()
13 registeredTokenStore = store17 registeredStore = store
14 storeMu.Unlock()18 storeMu.Unlock()
15}19}
16 20
17// GetTokenStore returns the globally registered token store.21// GetTokenStore returns the globally registered token store.
18func GetTokenStore() TokenStore {22func GetTokenStore() coreauth.Store {
19 storeMu.RLock()23 storeMu.RLock()
20 s := registeredTokenStore24 s := registeredStore
21 storeMu.RUnlock()25 storeMu.RUnlock()
22 if s != nil {26 if s != nil {
23 return s27 return s
24 }28 }
25 storeMu.Lock()29 storeMu.Lock()
26 defer storeMu.Unlock()30 defer storeMu.Unlock()
27 if registeredTokenStore == nil {31 if registeredStore == nil {
28 registeredTokenStore = NewFileTokenStore()32 registeredStore = NewFileTokenStore()
29 }33 }
30 return registeredTokenStore34 return registeredStore
31}35}
sdk/cliproxy/auth/manager.go+2−1
@@ -818,7 +818,8 @@ func (m *Manager) persist(ctx context.Context, auth *Auth) error { · 817 unmodified lines
818 if auth.Metadata == nil {818 if auth.Metadata == nil {
819 return nil819 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// StartAutoRefresh launches a background loop that evaluates auth freshness825// StartAutoRefresh launches a background loop that evaluates auth freshness
sdk/cliproxy/auth/store.go+2−2
@@ -6,8 +6,8 @@ import "context" · 5 unmodified lines
6type Store interface {6type Store interface {
7 // List returns all auth records stored in the backend.7 // List returns all auth records stored in the backend.
8 List(ctx context.Context) ([]*Auth, error)8 List(ctx context.Context) ([]*Auth, error)
9 // SaveAuth persists the provided auth record, replacing any existing one with same ID.9 // Save persists the provided auth record, replacing any existing one with same ID.
10 SaveAuth(ctx context.Context, auth *Auth) error10 Save(ctx context.Context, auth *Auth) (string, error)
11 // Delete removes the auth record identified by id.11 // Delete removes the auth record identified by id.
12 Delete(ctx context.Context, id string) error12 Delete(ctx context.Context, id string) error
13}13}
sdk/cliproxy/auth/types.go+6−0
@@ -6,6 +6,8 @@ import ( · 5 unmodified lines
6 "strings"6 "strings"
7 "sync"7 "sync"
8 "time"8 "time"
9
10 baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
9)11)
10 12
11// Auth encapsulates the runtime state and metadata associated with a single credential.13// Auth encapsulates the runtime state and metadata associated with a single credential.
@@ -14,6 +16,10 @@ type Auth struct { · 2 unmodified lines
14 ID string `json:"id"`16 ID string `json:"id"`
15 // Provider is the upstream provider key (e.g. "gemini", "claude").17 // Provider is the upstream provider key (e.g. "gemini", "claude").
16 Provider string `json:"provider"`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 // Label is an optional human readable label for logging.23 // Label is an optional human readable label for logging.
18 Label string `json:"label,omitempty"`24 Label string `json:"label,omitempty"`
19 // Status is the lifecycle status managed by the AuthManager.25 // Status is the lifecycle status managed by the AuthManager.
sdk/cliproxy/builder.go+1−5
@@ -197,11 +197,7 @@ func (b *Builder) Build() (*Service, error) { · 196 unmodified lines
197 if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {197 if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {
198 dirSetter.SetBaseDir(b.cfg.AuthDir)198 dirSetter.SetBaseDir(b.cfg.AuthDir)
199 }199 }
200 store, ok := tokenStore.(coreauth.Store)200 coreManager = coreauth.NewManager(tokenStore, nil, nil)
201 if !ok {
202 return nil, fmt.Errorf("cliproxy: token store does not implement coreauth.Store")
203 }
204 coreManager = coreauth.NewManager(store, nil, nil)
205 }201 }
206 // Attach a default RoundTripper provider so providers can opt-in per-auth transports.202 // Attach a default RoundTripper provider so providers can opt-in per-auth transports.
207 coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())203 coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())