package auth import ( "context" "fmt" "time" cursorauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor" "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" ) // CursorAuthenticator implements OAuth PKCE login for Cursor. type CursorAuthenticator struct{} // NewCursorAuthenticator constructs a new Cursor authenticator. func NewCursorAuthenticator() Authenticator { return &CursorAuthenticator{} } // Provider returns the provider key for cursor. func (CursorAuthenticator) Provider() string { return "cursor" } // RefreshLead returns the time before expiry when a refresh should be attempted. func (CursorAuthenticator) RefreshLead() *time.Duration { d := 10 * time.Minute return &d } // Login initiates the Cursor PKCE authentication flow. func (a CursorAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { if cfg == nil { return nil, fmt.Errorf("cursor auth: configuration is required") } if opts == nil { opts = &LoginOptions{} } // Generate PKCE auth parameters authParams, err := cursorauth.GenerateAuthParams() if err != nil { return nil, fmt.Errorf("cursor: failed to generate auth params: %w", err) } // Display the login URL log.Info("Starting Cursor authentication...") log.Infof("Please visit this URL to log in: %s", authParams.LoginURL) // Try to open the browser automatically if !opts.NoBrowser { if browser.IsAvailable() { if errOpen := browser.OpenURL(authParams.LoginURL); errOpen != nil { log.Warnf("Failed to open browser automatically: %v", errOpen) } } } log.Info("Waiting for Cursor authorization...") // Poll for the auth result tokens, err := cursorauth.PollForAuth(ctx, authParams.UUID, authParams.Verifier) if err != nil { return nil, fmt.Errorf("cursor: authentication failed: %w", err) } expiresAt := cursorauth.GetTokenExpiry(tokens.AccessToken) // Auto-identify account from JWT sub claim sub := cursorauth.ParseJWTSub(tokens.AccessToken) subHash := cursorauth.SubToShortHash(sub) log.Info("Cursor authentication successful!") metadata := map[string]any{ "type": "cursor", "access_token": tokens.AccessToken, "refresh_token": tokens.RefreshToken, "expires_at": expiresAt.Format(time.RFC3339), "timestamp": time.Now().UnixMilli(), } if sub != "" { metadata["sub"] = sub } fileName := cursorauth.CredentialFileName("", subHash) return &coreauth.Auth{ ID: fileName, Provider: a.Provider(), FileName: fileName, Label: cursorauth.DisplayLabel("", subHash), Metadata: metadata, }, nil }