oauth_hyper.go

 1package dialog
 2
 3import (
 4	"context"
 5	"fmt"
 6	"time"
 7
 8	tea "charm.land/bubbletea/v2"
 9	"github.com/charmbracelet/catwalk/pkg/catwalk"
10	"github.com/charmbracelet/crush/internal/config"
11	"github.com/charmbracelet/crush/internal/oauth/hyper"
12	"github.com/charmbracelet/crush/internal/ui/common"
13)
14
15func NewOAuthHyper(
16	com *common.Common,
17	isOnboarding bool,
18	provider catwalk.Provider,
19	model config.SelectedModel,
20	modelType config.SelectedModelType,
21) (*OAuth, tea.Cmd) {
22	return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthHyper{})
23}
24
25type OAuthHyper struct {
26	cancelFunc func()
27}
28
29var _ OAuthProvider = (*OAuthHyper)(nil)
30
31func (m *OAuthHyper) name() string {
32	return "Hyper"
33}
34
35func (m *OAuthHyper) initiateAuth() tea.Msg {
36	minimumWait := 750 * time.Millisecond
37	startTime := time.Now()
38
39	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
40	defer cancel()
41
42	authResp, err := hyper.InitiateDeviceAuth(ctx)
43
44	ellapsed := time.Since(startTime)
45	if ellapsed < minimumWait {
46		time.Sleep(minimumWait - ellapsed)
47	}
48
49	if err != nil {
50		return ActionOAuthErrored{fmt.Errorf("failed to initiate device auth: %w", err)}
51	}
52
53	return ActionInitiateOAuth{
54		DeviceCode:      authResp.DeviceCode,
55		UserCode:        authResp.UserCode,
56		ExpiresIn:       authResp.ExpiresIn,
57		VerificationURL: authResp.VerificationURL,
58	}
59}
60
61func (m *OAuthHyper) startPolling(deviceCode string, expiresIn int) tea.Cmd {
62	return func() tea.Msg {
63		ctx, cancel := context.WithCancel(context.Background())
64		m.cancelFunc = cancel
65
66		refreshToken, err := hyper.PollForToken(ctx, deviceCode, expiresIn)
67		if err != nil {
68			if ctx.Err() != nil {
69				return nil
70			}
71			return ActionOAuthErrored{err}
72		}
73
74		token, err := hyper.ExchangeToken(ctx, refreshToken)
75		if err != nil {
76			return ActionOAuthErrored{fmt.Errorf("token exchange failed: %w", err)}
77		}
78
79		introspect, err := hyper.IntrospectToken(ctx, token.AccessToken)
80		if err != nil {
81			return ActionOAuthErrored{fmt.Errorf("token introspection failed: %w", err)}
82		}
83		if !introspect.Active {
84			return ActionOAuthErrored{fmt.Errorf("access token is not active")}
85		}
86
87		return ActionCompleteOAuth{token}
88	}
89}
90
91func (m *OAuthHyper) stopPolling() tea.Msg {
92	if m.cancelFunc != nil {
93		m.cancelFunc()
94	}
95	return nil
96}