Skip to content

Commit cd0c94f

Browse files
fix(sdk/auth): prevent OAuth manual prompt goroutine leak,Use timer-based manual prompt per provider and remove oauth_callback helper.
1 parent 24970ba commit cd0c94f

File tree

5 files changed

+193
-98
lines changed

5 files changed

+193
-98
lines changed

sdk/auth/antigravity.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,54 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o
9999
fmt.Println("Waiting for antigravity authentication callback...")
100100

101101
var cbRes callbackResult
102-
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "antigravity")
103-
select {
104-
case res := <-cbChan:
105-
cbRes = res
106-
case manual := <-manualCh:
107-
cbRes = callbackResult{
108-
Code: manual.Code,
109-
State: manual.State,
110-
Error: manual.Error,
102+
timeoutTimer := time.NewTimer(5 * time.Minute)
103+
defer timeoutTimer.Stop()
104+
105+
var manualPromptTimer *time.Timer
106+
var manualPromptC <-chan time.Time
107+
if opts.Prompt != nil {
108+
manualPromptTimer = time.NewTimer(15 * time.Second)
109+
manualPromptC = manualPromptTimer.C
110+
defer manualPromptTimer.Stop()
111+
}
112+
113+
waitForCallback:
114+
for {
115+
select {
116+
case res := <-cbChan:
117+
cbRes = res
118+
break waitForCallback
119+
case <-manualPromptC:
120+
manualPromptC = nil
121+
if manualPromptTimer != nil {
122+
manualPromptTimer.Stop()
123+
}
124+
select {
125+
case res := <-cbChan:
126+
cbRes = res
127+
break waitForCallback
128+
default:
129+
}
130+
input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ")
131+
if errPrompt != nil {
132+
return nil, errPrompt
133+
}
134+
parsed, errParse := misc.ParseOAuthCallback(input)
135+
if errParse != nil {
136+
return nil, errParse
137+
}
138+
if parsed == nil {
139+
continue
140+
}
141+
cbRes = callbackResult{
142+
Code: parsed.Code,
143+
State: parsed.State,
144+
Error: parsed.Error,
145+
}
146+
break waitForCallback
147+
case <-timeoutTimer.C:
148+
return nil, fmt.Errorf("antigravity: authentication timed out")
111149
}
112-
case err = <-manualErrCh:
113-
return nil, err
114-
case <-time.After(5 * time.Minute):
115-
return nil, fmt.Errorf("antigravity: authentication timed out")
116150
}
117151

118152
if cbRes.Error != "" {

sdk/auth/claude.go

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
100100

101101
callbackCh := make(chan *claude.OAuthResult, 1)
102102
callbackErrCh := make(chan error, 1)
103-
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "Claude")
104103
manualDescription := ""
105104

106105
go func() {
@@ -113,22 +112,58 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
113112
}()
114113

115114
var result *claude.OAuthResult
116-
select {
117-
case result = <-callbackCh:
118-
case err = <-callbackErrCh:
119-
if strings.Contains(err.Error(), "timeout") {
120-
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
121-
}
122-
return nil, err
123-
case manual := <-manualCh:
124-
manualDescription = manual.ErrorDescription
125-
result = &claude.OAuthResult{
126-
Code: manual.Code,
127-
State: manual.State,
128-
Error: manual.Error,
115+
var manualPromptTimer *time.Timer
116+
var manualPromptC <-chan time.Time
117+
if opts.Prompt != nil {
118+
manualPromptTimer = time.NewTimer(15 * time.Second)
119+
manualPromptC = manualPromptTimer.C
120+
defer manualPromptTimer.Stop()
121+
}
122+
123+
waitForCallback:
124+
for {
125+
select {
126+
case result = <-callbackCh:
127+
break waitForCallback
128+
case err = <-callbackErrCh:
129+
if strings.Contains(err.Error(), "timeout") {
130+
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
131+
}
132+
return nil, err
133+
case <-manualPromptC:
134+
manualPromptC = nil
135+
if manualPromptTimer != nil {
136+
manualPromptTimer.Stop()
137+
}
138+
select {
139+
case result = <-callbackCh:
140+
break waitForCallback
141+
case err = <-callbackErrCh:
142+
if strings.Contains(err.Error(), "timeout") {
143+
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
144+
}
145+
return nil, err
146+
default:
147+
}
148+
input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ")
149+
if errPrompt != nil {
150+
return nil, errPrompt
151+
}
152+
parsed, errParse := misc.ParseOAuthCallback(input)
153+
if errParse != nil {
154+
return nil, errParse
155+
}
156+
if parsed == nil {
157+
continue
158+
}
159+
manualDescription = parsed.ErrorDescription
160+
result = &claude.OAuthResult{
161+
Code: parsed.Code,
162+
State: parsed.State,
163+
Error: parsed.Error,
164+
}
165+
break waitForCallback
129166
}
130-
case err = <-manualErrCh:
131-
return nil, err
132167
}
133168

134169
if result.Error != "" {

sdk/auth/codex.go

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
9999

100100
callbackCh := make(chan *codex.OAuthResult, 1)
101101
callbackErrCh := make(chan error, 1)
102-
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "Codex")
103102
manualDescription := ""
104103

105104
go func() {
@@ -112,22 +111,58 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
112111
}()
113112

114113
var result *codex.OAuthResult
115-
select {
116-
case result = <-callbackCh:
117-
case err = <-callbackErrCh:
118-
if strings.Contains(err.Error(), "timeout") {
119-
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
120-
}
121-
return nil, err
122-
case manual := <-manualCh:
123-
manualDescription = manual.ErrorDescription
124-
result = &codex.OAuthResult{
125-
Code: manual.Code,
126-
State: manual.State,
127-
Error: manual.Error,
114+
var manualPromptTimer *time.Timer
115+
var manualPromptC <-chan time.Time
116+
if opts.Prompt != nil {
117+
manualPromptTimer = time.NewTimer(15 * time.Second)
118+
manualPromptC = manualPromptTimer.C
119+
defer manualPromptTimer.Stop()
120+
}
121+
122+
waitForCallback:
123+
for {
124+
select {
125+
case result = <-callbackCh:
126+
break waitForCallback
127+
case err = <-callbackErrCh:
128+
if strings.Contains(err.Error(), "timeout") {
129+
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
130+
}
131+
return nil, err
132+
case <-manualPromptC:
133+
manualPromptC = nil
134+
if manualPromptTimer != nil {
135+
manualPromptTimer.Stop()
136+
}
137+
select {
138+
case result = <-callbackCh:
139+
break waitForCallback
140+
case err = <-callbackErrCh:
141+
if strings.Contains(err.Error(), "timeout") {
142+
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
143+
}
144+
return nil, err
145+
default:
146+
}
147+
input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ")
148+
if errPrompt != nil {
149+
return nil, errPrompt
150+
}
151+
parsed, errParse := misc.ParseOAuthCallback(input)
152+
if errParse != nil {
153+
return nil, errParse
154+
}
155+
if parsed == nil {
156+
continue
157+
}
158+
manualDescription = parsed.ErrorDescription
159+
result = &codex.OAuthResult{
160+
Code: parsed.Code,
161+
State: parsed.State,
162+
Error: parsed.Error,
163+
}
164+
break waitForCallback
128165
}
129-
case err = <-manualErrCh:
130-
return nil, err
131166
}
132167

133168
if result.Error != "" {

sdk/auth/iflow.go

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
8686

8787
callbackCh := make(chan *iflow.OAuthResult, 1)
8888
callbackErrCh := make(chan error, 1)
89-
manualCh, manualErrCh := promptForOAuthCallback(opts.Prompt, "iFlow")
9089

9190
go func() {
9291
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
@@ -98,18 +97,51 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
9897
}()
9998

10099
var result *iflow.OAuthResult
101-
select {
102-
case result = <-callbackCh:
103-
case err = <-callbackErrCh:
104-
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
105-
case manual := <-manualCh:
106-
result = &iflow.OAuthResult{
107-
Code: manual.Code,
108-
State: manual.State,
109-
Error: manual.Error,
100+
var manualPromptTimer *time.Timer
101+
var manualPromptC <-chan time.Time
102+
if opts.Prompt != nil {
103+
manualPromptTimer = time.NewTimer(15 * time.Second)
104+
manualPromptC = manualPromptTimer.C
105+
defer manualPromptTimer.Stop()
106+
}
107+
108+
waitForCallback:
109+
for {
110+
select {
111+
case result = <-callbackCh:
112+
break waitForCallback
113+
case err = <-callbackErrCh:
114+
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
115+
case <-manualPromptC:
116+
manualPromptC = nil
117+
if manualPromptTimer != nil {
118+
manualPromptTimer.Stop()
119+
}
120+
select {
121+
case result = <-callbackCh:
122+
break waitForCallback
123+
case err = <-callbackErrCh:
124+
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
125+
default:
126+
}
127+
input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ")
128+
if errPrompt != nil {
129+
return nil, errPrompt
130+
}
131+
parsed, errParse := misc.ParseOAuthCallback(input)
132+
if errParse != nil {
133+
return nil, errParse
134+
}
135+
if parsed == nil {
136+
continue
137+
}
138+
result = &iflow.OAuthResult{
139+
Code: parsed.Code,
140+
State: parsed.State,
141+
Error: parsed.Error,
142+
}
143+
break waitForCallback
110144
}
111-
case err = <-manualErrCh:
112-
return nil, err
113145
}
114146
if result.Error != "" {
115147
return nil, fmt.Errorf("iflow auth: provider returned error %s", result.Error)

sdk/auth/oauth_callback.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)