From a7bbda147eff0007de44c8694b3158e5824293b8 Mon Sep 17 00:00:00 2001 From: Jared Mahotiere Date: Thu, 12 Feb 2026 02:15:46 -0500 Subject: [PATCH] fix(auth): handle string interval in device-code login planned-date: 2026-02-12 why: Device-code login was failing because the interval field can arrive as a quoted number, which breaks strict integer decoding and blocks login in shell-only environments. what: Added flexible interval parsing for numeric or quoted values, wired LoginDeviceCode to the parser, printed the browser auth URL before waiting, and added parser tests for numeric, quoted, and invalid interval payloads. verification: c:\projects\toolchains\go\bin\go.exe test ./pkg/auth -run Test(ParseDeviceCodeResponse|BuildAuthorizeURL) --- pkg/auth/oauth.go | 63 ++++++++++++++++++++++++++++++++++++++---- pkg/auth/oauth_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index 94a79a6..ecd9ba2 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -13,6 +13,7 @@ import ( "net/url" "os/exec" "runtime" + "strconv" "strings" "time" ) @@ -92,10 +93,13 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) { server.Shutdown(ctx) }() + fmt.Printf("Open this URL to authenticate:\n\n%s\n\n", authURL) + if err := openBrowser(authURL); err != nil { fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL) } + fmt.Println("If you're running in a headless environment, use: picoclaw auth login --provider openai --device-code") fmt.Println("Waiting for authentication in browser...") select { @@ -114,6 +118,57 @@ type callbackResult struct { err error } +type deviceCodeResponse struct { + DeviceAuthID string + UserCode string + Interval int +} + +func parseDeviceCodeResponse(body []byte) (deviceCodeResponse, error) { + var raw struct { + DeviceAuthID string `json:"device_auth_id"` + UserCode string `json:"user_code"` + Interval json.RawMessage `json:"interval"` + } + + if err := json.Unmarshal(body, &raw); err != nil { + return deviceCodeResponse{}, err + } + + interval, err := parseFlexibleInt(raw.Interval) + if err != nil { + return deviceCodeResponse{}, err + } + + return deviceCodeResponse{ + DeviceAuthID: raw.DeviceAuthID, + UserCode: raw.UserCode, + Interval: interval, + }, nil +} + +func parseFlexibleInt(raw json.RawMessage) (int, error) { + if len(raw) == 0 || string(raw) == "null" { + return 0, nil + } + + var interval int + if err := json.Unmarshal(raw, &interval); err == nil { + return interval, nil + } + + var intervalStr string + if err := json.Unmarshal(raw, &intervalStr); err == nil { + intervalStr = strings.TrimSpace(intervalStr) + if intervalStr == "" { + return 0, nil + } + return strconv.Atoi(intervalStr) + } + + return 0, fmt.Errorf("invalid integer value: %s", string(raw)) +} + func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) { reqBody, _ := json.Marshal(map[string]string{ "client_id": cfg.ClientID, @@ -134,12 +189,8 @@ func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) { return nil, fmt.Errorf("device code request failed: %s", string(body)) } - var deviceResp struct { - DeviceAuthID string `json:"device_auth_id"` - UserCode string `json:"user_code"` - Interval int `json:"interval"` - } - if err := json.Unmarshal(body, &deviceResp); err != nil { + deviceResp, err := parseDeviceCodeResponse(body) + if err != nil { return nil, fmt.Errorf("parsing device code response: %w", err) } diff --git a/pkg/auth/oauth_test.go b/pkg/auth/oauth_test.go index 00b4c60..9f80132 100644 --- a/pkg/auth/oauth_test.go +++ b/pkg/auth/oauth_test.go @@ -197,3 +197,43 @@ func TestOpenAIOAuthConfig(t *testing.T) { t.Errorf("Port = %d, want 1455", cfg.Port) } } + +func TestParseDeviceCodeResponseIntervalAsNumber(t *testing.T) { + body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":5}`) + + resp, err := parseDeviceCodeResponse(body) + if err != nil { + t.Fatalf("parseDeviceCodeResponse() error: %v", err) + } + + if resp.DeviceAuthID != "abc" { + t.Errorf("DeviceAuthID = %q, want %q", resp.DeviceAuthID, "abc") + } + if resp.UserCode != "DEF-1234" { + t.Errorf("UserCode = %q, want %q", resp.UserCode, "DEF-1234") + } + if resp.Interval != 5 { + t.Errorf("Interval = %d, want %d", resp.Interval, 5) + } +} + +func TestParseDeviceCodeResponseIntervalAsString(t *testing.T) { + body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"5"}`) + + resp, err := parseDeviceCodeResponse(body) + if err != nil { + t.Fatalf("parseDeviceCodeResponse() error: %v", err) + } + + if resp.Interval != 5 { + t.Errorf("Interval = %d, want %d", resp.Interval, 5) + } +} + +func TestParseDeviceCodeResponseInvalidInterval(t *testing.T) { + body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"abc"}`) + + if _, err := parseDeviceCodeResponse(body); err == nil { + t.Fatal("expected error for invalid interval") + } +}