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
8 "net/http"8 "net/http"
9 "os"9 "os"
10 "path/filepath"10 "path/filepath"
11 "reflect"
12 "strings"11 "strings"
13 "sync"12 "sync"
14 "time"13 "time"
@@ -76,7 +75,7 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str · 61 unmodified lines
76 if existing, errRead := os.ReadFile(path); errRead == nil {75 if existing, errRead := os.ReadFile(path); errRead == nil {
77 // Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.76 // Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.
78 // This prevents the token refresh loop caused by timestamp/expired/expires_in changes.77 // 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) {
80 return path, nil79 return path, nil
81 }80 }
82 file, errOpen := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)81 file, errOpen := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)
@@ -300,28 +299,101 @@ func (s *FileTokenStore) baseDirSnapshot() string { · 217 unmodified lines
300 return s.baseDir299 return s.baseDir
301}300}
302 301
303// metadataEqualIgnoringTimestamps compares two metadata JSON blobs, ignoring volatile fields that302// DEPRECATED: Use metadataEqualIgnoringTimestamps for comparing auth metadata.
304// change on every refresh but don't affect authentication logic.303// This function is kept for backward compatibility but can cause refresh loops.
305func metadataEqualIgnoringTimestamps(a, b []byte) bool {304func jsonEqual(a, b []byte) bool {
306 var objA map[string]any305 var objA any
307 var objB map[string]any306 var objB any
308 if errUnmarshalA := json.Unmarshal(a, &objA); errUnmarshalA != nil {307 if err := json.Unmarshal(a, &objA); err != nil {
309 return false308 return false
310 }309 }
311 if errUnmarshalB := json.Unmarshal(b, &objB); errUnmarshalB != nil {310 if err := json.Unmarshal(b, &objB); err != nil {
312 return false311 return false
313 }312 }
314 stripVolatileMetadataFields(objA)313 return deepEqualJSON(objA, objB)
315 stripVolatileMetadataFields(objB)
316 return reflect.DeepEqual(objA, objB)
317}314}
318 315
319func stripVolatileMetadataFields(metadata map[string]any) {316// metadataEqualIgnoringTimestamps compares two metadata JSON blobs,
320 if metadata == nil {317// ignoring fields that change on every refresh but don't affect functionality.
321 return318// 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.
323func 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)
322 }346 }
323 // These fields change on refresh and would otherwise trigger watcher reload loops.347
324 for _, field := range []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"} {348 return deepEqualJSON(objA, objB)
325 delete(metadata, field)349}
350
351func 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
326 }398 }
327}399}