mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-08 06:43:41 +00:00
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
// Package kiro provides refresh utilities for Kiro token management.
|
|
package kiro
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// RefreshResult contains the result of a token refresh attempt.
|
|
type RefreshResult struct {
|
|
TokenData *KiroTokenData
|
|
Error error
|
|
UsedFallback bool // True if we used the existing token as fallback
|
|
}
|
|
|
|
// RefreshWithGracefulDegradation attempts to refresh a token with graceful degradation.
|
|
// If refresh fails but the existing access token is still valid, it returns the existing token.
|
|
// This matches kiro-openai-gateway's behavior for better reliability.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for the request
|
|
// - refreshFunc: Function to perform the actual refresh
|
|
// - existingAccessToken: Current access token (for fallback)
|
|
// - expiresAt: Expiration time of the existing token
|
|
//
|
|
// Returns:
|
|
// - RefreshResult containing the new or existing token data
|
|
func RefreshWithGracefulDegradation(
|
|
ctx context.Context,
|
|
refreshFunc func(ctx context.Context) (*KiroTokenData, error),
|
|
existingAccessToken string,
|
|
expiresAt time.Time,
|
|
) RefreshResult {
|
|
// Try to refresh the token
|
|
newTokenData, err := refreshFunc(ctx)
|
|
if err == nil {
|
|
return RefreshResult{
|
|
TokenData: newTokenData,
|
|
Error: nil,
|
|
UsedFallback: false,
|
|
}
|
|
}
|
|
|
|
// Refresh failed - check if we can use the existing token
|
|
log.Warnf("kiro: token refresh failed: %v", err)
|
|
|
|
// Check if existing token is still valid (not expired)
|
|
if existingAccessToken != "" && time.Now().Before(expiresAt) {
|
|
remainingTime := time.Until(expiresAt)
|
|
log.Warnf("kiro: using existing access token (expires in %v). Will retry refresh later.", remainingTime.Round(time.Second))
|
|
|
|
return RefreshResult{
|
|
TokenData: &KiroTokenData{
|
|
AccessToken: existingAccessToken,
|
|
ExpiresAt: expiresAt.Format(time.RFC3339),
|
|
},
|
|
Error: nil,
|
|
UsedFallback: true,
|
|
}
|
|
}
|
|
|
|
// Token is expired and refresh failed - return the error
|
|
return RefreshResult{
|
|
TokenData: nil,
|
|
Error: fmt.Errorf("token refresh failed and existing token is expired: %w", err),
|
|
UsedFallback: false,
|
|
}
|
|
}
|
|
|
|
// IsTokenExpiringSoon checks if a token is expiring within the given threshold.
|
|
// Default threshold is 5 minutes if not specified.
|
|
func IsTokenExpiringSoon(expiresAt time.Time, threshold time.Duration) bool {
|
|
if threshold == 0 {
|
|
threshold = 5 * time.Minute
|
|
}
|
|
return time.Now().Add(threshold).After(expiresAt)
|
|
}
|
|
|
|
// IsTokenExpired checks if a token has already expired.
|
|
func IsTokenExpired(expiresAt time.Time) bool {
|
|
return time.Now().After(expiresAt)
|
|
}
|
|
|
|
// ParseExpiresAt parses an expiration time string in RFC3339 format.
|
|
// Returns zero time if parsing fails.
|
|
func ParseExpiresAt(expiresAtStr string) time.Time {
|
|
if expiresAtStr == "" {
|
|
return time.Time{}
|
|
}
|
|
t, err := time.Parse(time.RFC3339, expiresAtStr)
|
|
if err != nil {
|
|
log.Debugf("kiro: failed to parse expiresAt '%s': %v", expiresAtStr, err)
|
|
return time.Time{}
|
|
}
|
|
return t
|
|
}
|
|
|
|
// RefreshConfig contains configuration for token refresh behavior.
|
|
type RefreshConfig struct {
|
|
// MaxRetries is the maximum number of refresh attempts (default: 1)
|
|
MaxRetries int
|
|
// RetryDelay is the delay between retry attempts (default: 1 second)
|
|
RetryDelay time.Duration
|
|
// RefreshThreshold is how early to refresh before expiration (default: 5 minutes)
|
|
RefreshThreshold time.Duration
|
|
// EnableGracefulDegradation allows using existing token if refresh fails (default: true)
|
|
EnableGracefulDegradation bool
|
|
}
|
|
|
|
// DefaultRefreshConfig returns the default refresh configuration.
|
|
func DefaultRefreshConfig() RefreshConfig {
|
|
return RefreshConfig{
|
|
MaxRetries: 1,
|
|
RetryDelay: time.Second,
|
|
RefreshThreshold: 5 * time.Minute,
|
|
EnableGracefulDegradation: true,
|
|
}
|
|
}
|
|
|
|
// RefreshWithRetry attempts to refresh a token with retry logic.
|
|
func RefreshWithRetry(
|
|
ctx context.Context,
|
|
refreshFunc func(ctx context.Context) (*KiroTokenData, error),
|
|
config RefreshConfig,
|
|
) (*KiroTokenData, error) {
|
|
var lastErr error
|
|
|
|
maxAttempts := config.MaxRetries + 1
|
|
if maxAttempts < 1 {
|
|
maxAttempts = 1
|
|
}
|
|
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
tokenData, err := refreshFunc(ctx)
|
|
if err == nil {
|
|
if attempt > 1 {
|
|
log.Infof("kiro: token refresh succeeded on attempt %d", attempt)
|
|
}
|
|
return tokenData, nil
|
|
}
|
|
|
|
lastErr = err
|
|
log.Warnf("kiro: token refresh attempt %d/%d failed: %v", attempt, maxAttempts, err)
|
|
|
|
// Don't sleep after the last attempt
|
|
if attempt < maxAttempts {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(config.RetryDelay):
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxAttempts, lastErr)
|
|
}
|