Skip to content

feat: browser-based login with automatic API key creation#43

Merged
jonathanpopham merged 2 commits intosupermodeltools:mainfrom
jonathanpopham:feat/browser-login
Apr 3, 2026
Merged

feat: browser-based login with automatic API key creation#43
jonathanpopham merged 2 commits intosupermodeltools:mainfrom
jonathanpopham:feat/browser-login

Conversation

@jonathanpopham
Copy link
Copy Markdown
Contributor

@jonathanpopham jonathanpopham commented Apr 3, 2026

Closes #35

What

supermodel login now opens the dashboard in the browser, which auto-creates an API key and sends it back to a localhost callback. Zero copy-paste needed.

Flow

$ supermodel login
Opening browser to log in...
If the browser doesn't open, visit:
  https://dashboard.supermodeltools.com/cli-auth?port=64060&state=...

Waiting for authentication...
✓ Authenticated — key saved to ~/.supermodel/config.yaml

How it works

  1. CLI starts a localhost HTTP server on a random port
  2. Opens browser to dashboard.supermodeltools.com/cli-auth?port={port}&state={state}
  3. Dashboard page checks auth (redirects to GitHub OAuth if needed), creates an API key, redirects to localhost:{port}/callback?key=...&state=...
  4. CLI receives the key, saves it, shows success
  5. Browser shows "You can close this tab"

Fallbacks

  • Browser can't open → falls back to manual paste
  • Timeout (5 min) → falls back to manual paste
  • CI/headless → supermodel login --token smsk_live_...

Dashboard change needed

The cli-auth page in supermodel-public-api has references to "uncompact" that need to be changed to "supermodel" (already changed locally, will PR separately).

Test plan

  • go test ./... — all 14 packages pass
  • Live test: browser flow completes end-to-end, key saved and works
  • --token flag works for CI
  • Manual fallback works when browser flow skipped

Summary by CodeRabbit

  • New Features

    • Browser-based login flow for a smoother interactive sign-in
    • Added a --token option for non-interactive/CI login
    • Manual fallback for environments where browser callback fails
  • Tests

    • New tests covering token login, callback handling, state generation, and logout behavior

supermodel login now opens the dashboard in the browser, which auto-creates
an API key and sends it back to a localhost callback server. Zero copy-paste.

Falls back to manual paste if the browser can't open or the flow times out
(5 minutes).

New: --token flag for CI/headless: supermodel login --token smsk_live_...

Closes supermodeltools#35
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Walkthrough

Browser-based login added with a localhost callback and a new non-interactive --token flag. cmd/login.go routes to LoginWithToken(token) when provided; otherwise Login(ctx) starts a local HTTP server, opens the dashboard, and receives the API key via /callback.

Changes

Cohort / File(s) Summary
Login command
cmd/login.go
Refactored command to use a local c variable and added a persistent --token string flag. Command now calls auth.LoginWithToken(token) when --token is set, otherwise invokes auth.Login(cmd.Context()).
Auth handler & helpers
internal/auth/handler.go
Replaced paste-prompt flow with browser-based localhost callback: starts HTTP server on 127.0.0.1:<port>, generates random state, opens dashboard URL, waits (<=5min) for /callback?key=...&state=..., validates state, saves trimmed key. Added LoginWithToken(token) and OS browser-launch helpers, random state generator, manual-paste fallback, and context/timeout handling.
Tests for auth flows
internal/auth/handler_test.go
New tests covering LoginWithToken validation, callback handling (valid/invalid state, missing key), randomState() properties, and Logout clearing stored API key. Tests isolate config by setting HOME to temp dirs.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI (login cmd)
    participant Local as Local HTTP<br/>(127.0.0.1:randomPort)
    participant Browser as Browser / Dashboard

    User->>CLI: run `supermodel login`
    activate CLI
    CLI->>Local: start HTTP server on random port
    CLI->>CLI: generate random state
    CLI->>Browser: open dashboard URL (port + state)
    Browser->>Browser: user authenticates & approves
    Browser->>Local: GET /callback?key=...&state=...
    Local->>Local: validate state, respond 200 "Authenticated"
    Local->>CLI: deliver key via channel
    CLI->>CLI: trim, validate, save key
    CLI->>User: show success
    deactivate CLI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🔑 A browser opens, a random state spun,
localhost waits for the callback to run.
Keys whisper in query strings at night,
CI brings --token for the headless light.
Smooth login now—no pasting, just one delight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change—implementing browser-based login with automatic API key creation, directly addressing issue #35's core objective.
Description check ✅ Passed Description covers all required template sections (What, Why, Test plan), includes clear flow diagrams, fallback strategies, and implementation details with test results documented.
Linked Issues check ✅ Passed Code changes implement the core objectives from #35: browser-based login flow with localhost callback, token flag for CI, fallback to manual paste, and key persistence in config.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the browser login flow and token flag; no unrelated modifications detected in login command, auth handler, or test additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/login.go`:
- Around line 19-23: The RunE handler currently checks token != "" which misses
the case where the user passed --token "" (empty) from CI; change the condition
to inspect whether the flag was explicitly provided using
cmd.Flags().Changed("token") and call auth.LoginWithToken(token) when true,
otherwise call auth.Login(cmd.Context()); update the anonymous RunE func in
cmd/login.go (the closure referencing the token variable) to use
cmd.Flags().Changed("token") so empty-but-provided tokens are forwarded to
auth.LoginWithToken for proper validation.

In `@internal/auth/handler.go`:
- Around line 49-61: The callback currently accepts a real API key via GET query
param ("key") and sends it over keyCh, which exposes the bearer token in browser
history; change the flow to accept a one-time code/session id instead (e.g.,
query param "code") and send that code over a new channel (e.g., codeCh) instead
of keyCh; implement a separate secure exchange endpoint (e.g., POST "/exchange")
or have the CLI poll a localhost endpoint to exchange that one-time code for the
real API key over HTTPS, and update any references to keyCh/"/callback" handler
to use the new codeCh and exchange flow so the actual bearer token is never
placed in a URL or browser history.
- Around line 64-69: The http.Server instance created as srv (currently
&http.Server{Handler: mux}) lacks a ReadHeaderTimeout, making it vulnerable to
Slowloris; update the server construction in the auth callback code to include
ReadHeaderTimeout: 5 * time.Second (e.g., &http.Server{Handler: mux,
ReadHeaderTimeout: 5 * time.Second}), ensure the time package is imported, and
keep existing behavior around listener and errCh/Srv.Serve unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2a05b23-d177-403d-8d7f-ae6d80b7934c

📥 Commits

Reviewing files that changed from the base of the PR and between a653527 and 345122a.

📒 Files selected for processing (2)
  • cmd/login.go
  • internal/auth/handler.go

Comment on lines 19 to 23
RunE: func(cmd *cobra.Command, _ []string) error {
if token != "" {
return auth.LoginWithToken(token)
}
return auth.Login(cmd.Context())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and inspect the login.go file
find . -name "login.go" -type f | head -20

Repository: supermodeltools/cli

Length of output: 77


🏁 Script executed:

# Check the structure of cmd directory
git ls-files cmd/ | head -20

Repository: supermodeltools/cli

Length of output: 303


🏁 Script executed:

# Read the login.go file to see the actual code
cat -n cmd/login.go

Repository: supermodeltools/cli

Length of output: 939


🏁 Script executed:

# Search for usage of Flags().Changed() in the codebase to confirm it's a valid API
rg "Changed\(" --type go | head -20

Repository: supermodeltools/cli

Length of output: 45


🏁 Script executed:

# Look at the auth package to see how LoginWithToken handles empty tokens
find . -path "*/internal/auth*" -type f | head -20

Repository: supermodeltools/cli

Length of output: 112


🏁 Script executed:

# Check the auth handler to see how LoginWithToken validates tokens
cat -n ./internal/auth/handler.go | head -100

Repository: supermodeltools/cli

Length of output: 3729


🏁 Script executed:

# Check if there's any token validation
rg "LoginWithToken|Login\(" --type go -A 5 | head -50

Repository: supermodeltools/cli

Length of output: 1031


🌐 Web query:

Cobra command line Changed() flag method documentation

💡 Result:

The Changed flag method in Cobra command line applications refers to functionality from the underlying pflag library (used by Cobra for flag parsing). There are two related features: 1. Flag.Changed field: A boolean field on the *pflag.Flag struct indicating whether the flag was explicitly set by the user during parsing (via command line arguments). It is set to true when FlagSet.Parse processes the flag, even if the provided value matches the default. Direct assignments to the bound variable or calls to flag.Value.Set do not set it. Initial value is false if not set during parse. Usage example (in a Cobra command's Run function, after parsing): flag := cmd.Flags.Lookup("myflag") if flag != nil && flag.Changed { // Flag was explicitly provided on command line } 2. FlagSet.Changed(name string) bool method: A method on *pflag.FlagSet (accessible via cmd.Flags or cmd.PersistentFlags) that returns true if the named flag was explicitly set during Parse, false otherwise. It looks up the flag and returns its Changed field. Usage: if cmd.Flags.Changed("myflag") { // Flag was set } This is useful for distinguishing user-provided flags from defaults, e.g., in conditional logic like printing only requested version fields (as discussed in Cobra issue #434). Note: Documentation comment "If the user set the value (or if left to default)" is ambiguous/misleading; behavior is "true if explicitly set during Parse". Official sources: pflag source code (flag.go) confirms flag.Changed = true is set during parsing when the flag is matched. pkg.go.dev/github.com/spf13/pflag documents the API. Cobra uses pflag internally, so this applies directly to cmd.Flags. Latest pflag v1.0.10 (2025), compatible with Cobra v1.10+.

Citations:


Check the flag was explicitly provided, not just that the string isn't empty.

Here's the thing: when you run supermodel login --token "$SUPERMODEL_API_KEY" in CI and that env var happens to be empty (or undefined), bash expands it to "". Your code sees token != "" as false, so it skips LoginWithToken() and tries to open a browser instead. In a headless CI box with no browser, this just hangs forever waiting for something that'll never happen—super confusing for whoever's debugging the CI job.

The auth package is actually ready for this—LoginWithToken() already checks for empty tokens and returns a clear error saying "API key cannot be empty". But your flag check never gets there.

Use cmd.Flags().Changed("token") to check whether the user actually provided the flag (even if it's empty). This way, if someone passes --token "", it'll hit LoginWithToken(), which rejects it with a helpful error message instead of silently hanging.

Suggested fix
 		RunE: func(cmd *cobra.Command, _ []string) error {
-			if token != "" {
+			if cmd.Flags().Changed("token") {
 				return auth.LoginWithToken(token)
 			}
 			return auth.Login(cmd.Context())
 		},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RunE: func(cmd *cobra.Command, _ []string) error {
if token != "" {
return auth.LoginWithToken(token)
}
return auth.Login(cmd.Context())
RunE: func(cmd *cobra.Command, _ []string) error {
if cmd.Flags().Changed("token") {
return auth.LoginWithToken(token)
}
return auth.Login(cmd.Context())
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/login.go` around lines 19 - 23, The RunE handler currently checks token
!= "" which misses the case where the user passed --token "" (empty) from CI;
change the condition to inspect whether the flag was explicitly provided using
cmd.Flags().Changed("token") and call auth.LoginWithToken(token) when true,
otherwise call auth.Login(cmd.Context()); update the anonymous RunE func in
cmd/login.go (the closure referencing the token variable) to use
cmd.Flags().Changed("token") so empty-but-provided tokens are forwarded to
auth.LoginWithToken for proper validation.

Comment on lines +49 to +61
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return
}
key := r.URL.Query().Get("key")
if key == "" {
http.Error(w, "Missing key", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fff"><div style="text-align:center"><h2>&#10003; Authenticated</h2><p style="color:#888">You can close this tab and return to your terminal.</p></div></body></html>`)
keyCh <- key
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t round-trip the real API key through GET /callback?key=....

Accepting the key from the query string means the dashboard is putting a live bearer token into browser history and any local URL logging on the machine. That should be a one-time code/session ID that the CLI exchanges for the real key over HTTPS, or a POST body to localhost if you want to keep the direct callback shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler.go` around lines 49 - 61, The callback currently
accepts a real API key via GET query param ("key") and sends it over keyCh,
which exposes the bearer token in browser history; change the flow to accept a
one-time code/session id instead (e.g., query param "code") and send that code
over a new channel (e.g., codeCh) instead of keyCh; implement a separate secure
exchange endpoint (e.g., POST "/exchange") or have the CLI poll a localhost
endpoint to exchange that one-time code for the real API key over HTTPS, and
update any references to keyCh/"/callback" handler to use the new codeCh and
exchange flow so the actual bearer token is never placed in a URL or browser
history.

Comment on lines +86 to +93
case key := <-keyCh:
fmt.Println()
cfg.APIKey = strings.TrimSpace(key)
if err := cfg.Save(); err != nil {
return err
}
ui.Success("Authenticated — key saved to %s", config.Path())
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the key before saving it and printing “Authenticated”.

All three paths only do trim/non-empty checks before cfg.Save(). internal/config/config.go:57-71 writes cfg.APIKey straight to disk, and internal/api/client.go:27-34 consumes it as-is on later requests, so a bad callback payload or pasted typo becomes a persisted broken login. Please funnel these through a shared validate-and-save step so login only succeeds once the key is known-good.

Also applies to: 109-123, 145-163

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
internal/auth/handler_test.go (3)

49-49: Consider not ignoring the error from config.Load().

I know this is test code and it'll probably be fine, but ignoring errors can hide issues when debugging flaky tests later. A quick check would make this more robust:

-	cfg, _ := config.Load()
+	cfg, err := config.Load()
+	if err != nil {
+		t.Fatal(err)
+	}

Same thing applies to line 181.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler_test.go` at line 49, The test currently ignores the
error returned by config.Load(); update the test to check the returned error
(from config.Load()) and fail the test if non-nil—e.g., capture (cfg, err :=
config.Load()) and use t.Fatalf/t.Fatal or a test assertion helper
(require.NoError/require.Nil) to surface failures; apply the same change for the
second ignored call at the other occurrence so both places properly validate the
load error before proceeding.

67-80: This duplicates the callback handler logic instead of testing the real one.

So right now you're defining a fresh handler inline that looks like the real /callback handler in handler.go, but it's actually a separate copy. If someone tweaks the real handler (say, adds a new validation), this test would still pass even if the real code is broken.

Ideally, you'd extract the callback handler logic into a reusable function or test the actual Login() flow end-to-end. I get that testing the full browser flow is tricky, but maybe you could export the handler setup or use a factory function?

Something like:

// In handler.go
func newCallbackHandler(expectedState string, keyCh chan<- string) http.HandlerFunc { ... }

// In test
mux.HandleFunc("/callback", newCallbackHandler(state, keyCh))

This way you're testing the actual code path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler_test.go` around lines 67 - 80, The test duplicates the
/callback logic instead of exercising the real handler; extract the callback
logic from handler.go into a reusable function (e.g.,
newCallbackHandler(expectedState string, keyCh chan<- string) http.HandlerFunc)
and have the production Login() setup use that factory, then replace the inline
handler in internal/auth/handler_test.go with mux.HandleFunc("/callback",
newCallbackHandler(state, keyCh)) so the test exercises the actual code path.

174-175: These errors should probably be checked.

I know it's test code, but if the directory can't be created or config can't be saved, the test will fail in a confusing way later. Quick fix:

-	os.MkdirAll(filepath.Join(tmp, ".supermodel"), 0o700)
-	cfg.Save()
+	if err := os.MkdirAll(filepath.Join(tmp, ".supermodel"), 0o700); err != nil {
+		t.Fatal(err)
+	}
+	if err := cfg.Save(); err != nil {
+		t.Fatal(err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler_test.go` around lines 174 - 175, The test currently
ignores errors from os.MkdirAll(filepath.Join(tmp, ".supermodel"), 0o700) and
cfg.Save(); capture and check both return values and fail the test immediately
on error (e.g., use t.Fatalf or testing helper like require.NoError) so failures
to create the directory or persist config produce a clear error at this point;
update the call sites around os.MkdirAll and cfg.Save() in handler_test.go to
handle their errors and abort the test on failure.
internal/auth/handler.go (1)

64-70: The //nolint:gosec comment is probably unnecessary now.

Since you've got ReadHeaderTimeout: 10 * time.Second set, the Slowloris attack warning (G112) that gosec would normally raise should pass without any suppression. The nolint comment might be leftover from before the timeout was added.

Two options:

  1. Remove it if gosec is happy with the timeout (which it should be)
  2. Keep it and be specific with a rule ID like //nolint:gosec // G112 if there's a specific reason it needs to stay

Generic //nolint:gosec comments without a rule ID make it harder for future readers to understand why the suppression is there, so either approach is better than leaving it as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler.go` around lines 64 - 70, The generic suppression
comment "//nolint:gosec" next to the server construction is likely unnecessary
now that ReadHeaderTimeout is set; remove the generic nolint or make it
rule-specific. Update the srv creation in internal/auth/handler.go (the
http.Server value assigned to variable srv with ReadHeaderTimeout: 10 *
time.Second) by either deleting the "//nolint:gosec" comment entirely or
replacing it with a rule-specific suppression like "//nolint:gosec // G112" if
you have a documented reason to silence the Slowloris check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auth/handler_test.go`:
- Around line 17-33: The tests TestLoginWithToken,
TestLoginWithToken_Whitespace, and TestLogout currently call t.Setenv("HOME",
tmp) which doesn't isolate the home directory on Windows; update each test to
also set USERPROFILE to the same tmp (i.e., call t.Setenv("USERPROFILE", tmp)
right after t.Setenv("HOME", tmp)) so os.UserHomeDir() will point to the temp
dir on Windows and avoid touching real config during LoginWithToken and Logout
flows.

In `@internal/auth/handler.go`:
- Around line 179-183: The randomState function currently ignores the error from
rand.Read causing possible non-random state; update randomState to check the
error returned by crypto/rand.Read (in function randomState), and if err != nil
panic with a clear message including the error so the process fails fast rather
than returning a weak state string.

---

Nitpick comments:
In `@internal/auth/handler_test.go`:
- Line 49: The test currently ignores the error returned by config.Load();
update the test to check the returned error (from config.Load()) and fail the
test if non-nil—e.g., capture (cfg, err := config.Load()) and use
t.Fatalf/t.Fatal or a test assertion helper (require.NoError/require.Nil) to
surface failures; apply the same change for the second ignored call at the other
occurrence so both places properly validate the load error before proceeding.
- Around line 67-80: The test duplicates the /callback logic instead of
exercising the real handler; extract the callback logic from handler.go into a
reusable function (e.g., newCallbackHandler(expectedState string, keyCh chan<-
string) http.HandlerFunc) and have the production Login() setup use that
factory, then replace the inline handler in internal/auth/handler_test.go with
mux.HandleFunc("/callback", newCallbackHandler(state, keyCh)) so the test
exercises the actual code path.
- Around line 174-175: The test currently ignores errors from
os.MkdirAll(filepath.Join(tmp, ".supermodel"), 0o700) and cfg.Save(); capture
and check both return values and fail the test immediately on error (e.g., use
t.Fatalf or testing helper like require.NoError) so failures to create the
directory or persist config produce a clear error at this point; update the call
sites around os.MkdirAll and cfg.Save() in handler_test.go to handle their
errors and abort the test on failure.

In `@internal/auth/handler.go`:
- Around line 64-70: The generic suppression comment "//nolint:gosec" next to
the server construction is likely unnecessary now that ReadHeaderTimeout is set;
remove the generic nolint or make it rule-specific. Update the srv creation in
internal/auth/handler.go (the http.Server value assigned to variable srv with
ReadHeaderTimeout: 10 * time.Second) by either deleting the "//nolint:gosec"
comment entirely or replacing it with a rule-specific suppression like
"//nolint:gosec // G112" if you have a documented reason to silence the
Slowloris check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33ffa448-3e7c-41ad-a336-2cab4024b691

📥 Commits

Reviewing files that changed from the base of the PR and between 345122a and b71ea56.

📒 Files selected for processing (2)
  • internal/auth/handler.go
  • internal/auth/handler_test.go

Comment on lines +17 to +33
func TestLoginWithToken(t *testing.T) {
// Point config to a temp dir so we don't touch real config.
tmp := t.TempDir()
t.Setenv("HOME", tmp)

if err := LoginWithToken("smsk_live_test123"); err != nil {
t.Fatalf("LoginWithToken: %v", err)
}

cfg, err := config.Load()
if err != nil {
t.Fatal(err)
}
if cfg.APIKey != "smsk_live_test123" {
t.Errorf("expected key smsk_live_test123, got %q", cfg.APIKey)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What environment variables does Go's os.UserHomeDir() check on Windows?

💡 Result:

On Windows, Go's os.UserHomeDir checks the USERPROFILE environment variable. If USERPROFILE is set and non-empty, it returns its value. If not set, it returns an error indicating that %USERPROFILE% is not defined. This is confirmed by the official Go documentation and the source code implementation in src/os/file.go: func UserHomeDir (string, error) { ... switch runtime.GOOS { case "windows": env, enverr = "USERPROFILE", "%userprofile%" } if v := Getenv(env); v != "" { return v, nil } ... return "", errors.New(enverr + " is not defined") } Note that older versions (pre-Go 1.12.1) used HOMEDRIVE + HOMEPATH, but since Go 1.12.1, it uses USERPROFILE exclusively. As of 2026, all maintained Go versions use USERPROFILE.

Citations:


Fix Windows test isolation by setting USERPROFILE env var.

The test uses t.Setenv("HOME", tmp) to isolate config, but this doesn't work on Windows. The Go standard library's os.UserHomeDir() function reads HOME on Unix-like systems, but on Windows it only reads the USERPROFILE environment variable. So on Windows, the test would still touch the real config file even though we're trying to prevent it.

The fix is straightforward—just set both variables:

t.Setenv("HOME", tmp)
t.Setenv("USERPROFILE", tmp)

This applies to TestLoginWithToken, TestLoginWithToken_Whitespace, and TestLogout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler_test.go` around lines 17 - 33, The tests
TestLoginWithToken, TestLoginWithToken_Whitespace, and TestLogout currently call
t.Setenv("HOME", tmp) which doesn't isolate the home directory on Windows;
update each test to also set USERPROFILE to the same tmp (i.e., call
t.Setenv("USERPROFILE", tmp) right after t.Setenv("HOME", tmp)) so
os.UserHomeDir() will point to the temp dir on Windows and avoid touching real
config during LoginWithToken and Logout flows.

Comment on lines +179 to +183
func randomState() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The error from rand.Read is ignored.

crypto/rand.Read can fail in edge cases (like entropy pool exhaustion on some systems). If it fails, you'd get a zero or partially-filled buffer, which means your state parameter wouldn't be random at all. That could weaken the CSRF protection.

 func randomState() string {
 	b := make([]byte, 16)
-	_, _ = rand.Read(b)
+	if _, err := rand.Read(b); err != nil {
+		panic("failed to generate random state: " + err.Error())
+	}
 	return hex.EncodeToString(b)
 }

A panic is fine here because if you can't get random bytes, something is seriously wrong with the system and you shouldn't proceed with auth anyway.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/handler.go` around lines 179 - 183, The randomState function
currently ignores the error from rand.Read causing possible non-random state;
update randomState to check the error returned by crypto/rand.Read (in function
randomState), and if err != nil panic with a clear message including the error
so the process fails fast rather than returning a weak state string.

@jonathanpopham jonathanpopham marked this pull request as ready for review April 3, 2026 21:48
@jonathanpopham jonathanpopham merged commit 9d9b5cf into supermodeltools:main Apr 3, 2026
7 checks passed
jonathanpopham added a commit that referenced this pull request Apr 3, 2026
jonathanpopham added a commit that referenced this pull request Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: streamlined sign-in with dashboard API key generation

1 participant