From 33aa665555e4ea6e2687cb2d6355d39f6fe5df72 Mon Sep 17 00:00:00 2001 From: Zhi Yang <196515526+FakerL@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:21:29 +0000 Subject: [PATCH] fix(auth): persist access_token on refresh for providers that need it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, metadataEqualIgnoringTimestamps() ignored access_token for all providers, which prevented refreshed tokens from being persisted to disk/database. This caused tokens to be lost on server restart for providers like iFlow. This change makes the behavior provider-specific: - Providers like gemini/gemini-cli that issue new tokens on every refresh and can re-fetch when needed will continue to ignore access_token (optimization) - Other providers like iFlow will now persist access_token changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sdk/auth/filestore.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go index 84092d37..772de9d6 100644 --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -74,7 +74,7 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str if existing, errRead := os.ReadFile(path); errRead == nil { // Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change. // This prevents the token refresh loop caused by timestamp/expired/expires_in changes. - if metadataEqualIgnoringTimestamps(existing, raw) { + if metadataEqualIgnoringTimestamps(existing, raw, auth.Provider) { return path, nil } } else if errRead != nil && !os.IsNotExist(errRead) { @@ -284,7 +284,10 @@ func jsonEqual(a, b []byte) bool { // ignoring fields that change on every refresh but don't affect functionality. // This prevents unnecessary file writes that would trigger watcher events and // create refresh loops. -func metadataEqualIgnoringTimestamps(a, b []byte) bool { +// The provider parameter controls whether access_token is ignored: providers like +// Google OAuth (gemini, gemini-cli) can re-fetch tokens when needed, while others +// like iFlow require the refreshed token to be persisted. +func metadataEqualIgnoringTimestamps(a, b []byte, provider string) bool { var objA, objB map[string]any if err := json.Unmarshal(a, &objA); err != nil { return false @@ -295,9 +298,18 @@ func metadataEqualIgnoringTimestamps(a, b []byte) bool { // Fields to ignore: these change on every refresh but don't affect authentication logic. // - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh - // - access_token: Google OAuth returns a new access_token on each refresh, this is expected - // and shouldn't trigger file writes (the new token will be fetched again when needed) - ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"} + ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh"} + + // Providers that issue new access_token on every refresh and can re-fetch when needed. + // For these providers, we also ignore access_token to avoid unnecessary file writes. + providersIgnoringAccessToken := map[string]bool{ + "gemini": true, + "gemini-cli": true, + } + if providersIgnoringAccessToken[provider] { + ignoredFields = append(ignoredFields, "access_token") + } + for _, field := range ignoredFields { delete(objA, field) delete(objB, field)