package auth import ( "context" "fmt" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" ) // kimiRefreshLead is the duration before token expiry when refresh should occur. var kimiRefreshLead = 5 * time.Minute // KimiAuthenticator implements the OAuth device flow login for Kimi (Moonshot AI). type KimiAuthenticator struct{} // NewKimiAuthenticator constructs a new Kimi authenticator. func NewKimiAuthenticator() Authenticator { return &KimiAuthenticator{} } // Provider returns the provider key for kimi. func (KimiAuthenticator) Provider() string { return "kimi" } // RefreshLead returns the duration before token expiry when refresh should occur. // Kimi tokens expire and need to be refreshed before expiry. func (KimiAuthenticator) RefreshLead() *time.Duration { return &kimiRefreshLead } // Login initiates the Kimi device flow authentication. func (a KimiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { if cfg == nil { return nil, fmt.Errorf("cliproxy auth: configuration is required") } if opts == nil { opts = &LoginOptions{} } authSvc := kimi.NewKimiAuth(cfg) // Start the device flow fmt.Println("Starting Kimi authentication...") deviceCode, err := authSvc.StartDeviceFlow(ctx) if err != nil { return nil, fmt.Errorf("kimi: failed to start device flow: %w", err) } // Display the verification URL verificationURL := deviceCode.VerificationURIComplete if verificationURL == "" { verificationURL = deviceCode.VerificationURI } fmt.Printf("\nTo authenticate, please visit:\n%s\n\n", verificationURL) if deviceCode.UserCode != "" { fmt.Printf("User code: %s\n\n", deviceCode.UserCode) } // Try to open the browser automatically if !opts.NoBrowser { if browser.IsAvailable() { if errOpen := browser.OpenURL(verificationURL); errOpen != nil { log.Warnf("Failed to open browser automatically: %v", errOpen) } else { fmt.Println("Browser opened automatically.") } } } fmt.Println("Waiting for authorization...") if deviceCode.ExpiresIn > 0 { fmt.Printf("(This will timeout in %d seconds if not authorized)\n", deviceCode.ExpiresIn) } // Wait for user authorization authBundle, err := authSvc.WaitForAuthorization(ctx, deviceCode) if err != nil { return nil, fmt.Errorf("kimi: %w", err) } // Create the token storage tokenStorage := authSvc.CreateTokenStorage(authBundle) // Build metadata with token information metadata := map[string]any{ "type": "kimi", "access_token": authBundle.TokenData.AccessToken, "refresh_token": authBundle.TokenData.RefreshToken, "token_type": authBundle.TokenData.TokenType, "scope": authBundle.TokenData.Scope, "timestamp": time.Now().UnixMilli(), } if authBundle.TokenData.ExpiresAt > 0 { exp := time.Unix(authBundle.TokenData.ExpiresAt, 0).UTC().Format(time.RFC3339) metadata["expired"] = exp } if strings.TrimSpace(authBundle.DeviceID) != "" { metadata["device_id"] = strings.TrimSpace(authBundle.DeviceID) } // Generate a unique filename fileName := fmt.Sprintf("kimi-%d.json", time.Now().UnixMilli()) fmt.Println("\nKimi authentication successful!") return &coreauth.Auth{ ID: fileName, Provider: a.Provider(), FileName: fileName, Label: "Kimi User", Storage: tokenStorage, Metadata: metadata, }, nil }