History

Merge pull request #869

Luis Pater committed 9 months ago

Commit 7beaf0e2 parents 1fef90f 08d21b71 file · +90 −18Browse files

Unified · Split

sdk/auth/filestore.go+90−18
@@ -8,7 +8,6 @@ import ( · 7 unmodified lines
88 "net/http"
99 "os"
1010 "path/filepath"
11− "reflect"
1211 "strings"
1312 "sync"
1413 "time"
@@ -76,7 +75,7 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str · 61 unmodified lines
7675 if existing, errRead := os.ReadFile(path); errRead == nil {
7776 // Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.
7877 // This prevents the token refresh loop caused by timestamp/expired/expires_in changes.
79− if metadataEqualIgnoringTimestamps(existing, raw) {
78+ if metadataEqualIgnoringTimestamps(existing, raw, auth.Provider) {
8079 return path, nil
8180 }
8281 file, errOpen := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)
@@ -300,28 +299,101 @@ func (s *FileTokenStore) baseDirSnapshot() string { · 217 unmodified lines
300299 return s.baseDir
301300 }
302301
303−// metadataEqualIgnoringTimestamps compares two metadata JSON blobs, ignoring volatile fields that
304−// change on every refresh but don't affect authentication logic.
305−func metadataEqualIgnoringTimestamps(a, b []byte) bool {
306− var objA map[string]any
307− var objB map[string]any
308− if errUnmarshalA := json.Unmarshal(a, &objA); errUnmarshalA != nil {
302+// DEPRECATED: Use metadataEqualIgnoringTimestamps for comparing auth metadata.
303+// This function is kept for backward compatibility but can cause refresh loops.
304+func jsonEqual(a, b []byte) bool {
305+ var objA any
306+ var objB any
307+ if err := json.Unmarshal(a, &objA); err != nil {
309308 return false
310309 }
311− if errUnmarshalB := json.Unmarshal(b, &objB); errUnmarshalB != nil {
310+ if err := json.Unmarshal(b, &objB); err != nil {
312311 return false
313312 }
314− stripVolatileMetadataFields(objA)
315− stripVolatileMetadataFields(objB)
316− return reflect.DeepEqual(objA, objB)
313+ return deepEqualJSON(objA, objB)
317314 }
318315
319−func stripVolatileMetadataFields(metadata map[string]any) {
320− if metadata == nil {
321− return
316+// metadataEqualIgnoringTimestamps compares two metadata JSON blobs,
317+// ignoring fields that change on every refresh but don't affect functionality.
318+// This prevents unnecessary file writes that would trigger watcher events and
319+// create refresh loops.
320+// The provider parameter controls whether access_token is ignored: providers like
321+// Google OAuth (gemini, gemini-cli) can re-fetch tokens when needed, while others
322+// like iFlow require the refreshed token to be persisted.
323+func metadataEqualIgnoringTimestamps(a, b []byte, provider string) bool {
324+ var objA, objB map[string]any
325+ if err := json.Unmarshal(a, &objA); err != nil {
326+ return false
327+ }
328+ if err := json.Unmarshal(b, &objB); err != nil {
329+ return false
330+ }
331+
332+ // Fields to ignore: these change on every refresh but don't affect authentication logic.
333+ // - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh
334+ ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh"}
335+
336+ // For providers that can re-fetch tokens when needed (e.g., Google OAuth),
337+ // we ignore access_token to avoid unnecessary file writes.
338+ switch provider {
339+ case "gemini", "gemini-cli":
340+ ignoredFields = append(ignoredFields, "access_token")
341+ }
342+
343+ for _, field := range ignoredFields {
344+ delete(objA, field)
345+ delete(objB, field)
322346 }
323− // These fields change on refresh and would otherwise trigger watcher reload loops.
324− for _, field := range []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"} {
325− delete(metadata, field)
347+
348+ return deepEqualJSON(objA, objB)
349+}
350+
351+func deepEqualJSON(a, b any) bool {
352+ switch valA := a.(type) {
353+ case map[string]any:
354+ valB, ok := b.(map[string]any)
355+ if !ok || len(valA) != len(valB) {
356+ return false
357+ }
358+ for key, subA := range valA {
359+ subB, ok1 := valB[key]
360+ if !ok1 || !deepEqualJSON(subA, subB) {
361+ return false
362+ }
363+ }
364+ return true
365+ case []any:
366+ sliceB, ok := b.([]any)
367+ if !ok || len(valA) != len(sliceB) {
368+ return false
369+ }
370+ for i := range valA {
371+ if !deepEqualJSON(valA[i], sliceB[i]) {
372+ return false
373+ }
374+ }
375+ return true
376+ case float64:
377+ valB, ok := b.(float64)
378+ if !ok {
379+ return false
380+ }
381+ return valA == valB
382+ case string:
383+ valB, ok := b.(string)
384+ if !ok {
385+ return false
386+ }
387+ return valA == valB
388+ case bool:
389+ valB, ok := b.(bool)
390+ if !ok {
391+ return false
392+ }
393+ return valA == valB
394+ case nil:
395+ return b == nil
396+ default:
397+ return false
326398 }
327399 }