# login: drop ireturn nolint via concrete flow; tidy output spacing

`ec2a59f`→[main](/content/gh/entireio/cli/commits/main/index.html)·
toothbrush·1mo ago·3 files·+44 added/-83 removed

Avoid the ireturn lint suppression by following "accept interfaces, return structs" (as the device-flow shim already does):

- auth.BrowserAuthFlow is now a concrete struct (was an interface);
  StartBrowserAuth returns \*BrowserAuthFlow.
- runBrowserLogin accepts a cli-local browserAuthFlow interface plus the
  base URL, with StartBrowserAuth moved up into newLoginCmd. The fake
  flow satisfies the local interface; no nolint anywhere.

Output: one space after "Logging in to:" and a single blank line before
"Waiting for sign-in..." (the leading newline doubled up with the
post-Enter newline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

e72efb851121View transcript

## Changes

3

- cmd/entire/cli

- auth

- Mclient.go+17/-28

- Mlogin.go+22/-18

- Mlogin_test.go+5/-37

```go
92 unmodified lines
```

// BrowserAuthFlow is one in-progress loopback authorization-code login.
// It wraps an authcode.Flow so login.go can depend on a small interface
// (and fake it in tests) rather than the concrete library type that owns
// a live loopback listener.
type BrowserAuthFlow interface {
    // AuthorizationURL is the URL to open in the user's browser.
    AuthorizationURL() string
    // Wait blocks until the browser is redirected to the loopback
    // listener, returning the authorization code.
    Wait(ctx context.Context) (code string, err error)
    // Exchange redeems code for access + refresh tokens.
    Exchange(ctx context.Context, code string) (accessToken, refreshToken string, err error)
    // Close tears down the loopback listener. Safe to call after Wait.
    Close() error
}

// browserAuthFlow adapts *authcode.Flow to BrowserAuthFlow, flattening the
// TokenSet to the (access, refresh) pair login.go persists — mirroring how
// PollDeviceAuth flattens the device-flow result.
type browserAuthFlow struct {
    // BrowserAuthFlow is one in-progress loopback authorization-code login. It
    // wraps an authcode.Flow, flattening the TokenSet to the (access, refresh)
    // pair login.go persists — mirroring how PollDeviceAuth flattens the
    // device-flow result. login.go depends on a small local interface that this
    // concrete type satisfies, so it can fake the flow in tests.
type BrowserAuthFlow struct {
    inner *authcode.Flow
}

func (f *browserAuthFlow) AuthorizationURL() string { return f.inner.AuthorizationURL }

func (f *browserAuthFlow) Wait(ctx context.Context) (string, error) {
    return f.inner.Wait(ctx) //nolint:wrapcheck // shim preserves the lib's wrapped errors verbatim for errors.Is
}

func (f *browserAuthFlow) Exchange(ctx context.Context, code string) (string, string, error) {
    ts, err := f.inner.Exchange(ctx, code)
    if err != nil {
        return "", "", err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels
    }
    return ts.AccessToken, ts.RefreshToken, nil
}

func (f *browserAuthFlow) Close() error {
    return f.inner.Close() //nolint:wrapcheck // shutdown error is best-effort; caller logs at most
}

// StartBrowserAuth begins the loopback authorization-code flow: it binds a
// local listener and returns a flow carrying the browser URL to open.
func (c *Client) StartBrowserAuth(ctx context.Context) (BrowserAuthFlow, error) {
    f, err := c.browser.Start(ctx)
    if err != nil {
        return nil, err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels
    }
    return &browserAuthFlow{inner: f}, nil
}

// BaseURL returns the issuer base URL this client talks to.

Mcmd/entire/cli/auth/client.go+17/-28

```go
46 unmodified lines
```

type browserAuthClient interface {
    StartBrowserAuth(ctx context.Context) (auth.BrowserAuthFlow, error)
    BaseURL() string
}

func newLoginCmd() *cobra.Command {
    // the same both-flows-with-fallback shape gh / gcloud / aws sso
    // ship. --device forces the device flow explicitly.
}

// runBrowserLogin runs the loopback authorization-code flow: open the
// authorization URL in the user's browser, wait for the redirect back to
// the local listener, then exchange the code for tokens.
func runBrowserLogin(ctx context.Context, outW, errW io.Writer, client browserAuthClient, openURL browserOpenFunc) error {
    flow, err := client.StartBrowserAuth(ctx)
    if err != nil {
        return fmt.Errorf("start login: %w", err)
    }
    defer func() { _ = flow.Close() }()
    fmt.Fprintf(outW, "Logging in to:  %s\n\n", client.BaseURL())
    fmt.Fprint(outW, "Press Enter to open in browser...")

code, err := flow.Wait(ctx)
    if err != nil {
        return fmt.Errorf("complete login: %w", err)
    }
    return persistLogin(outW, errW, client.BaseURL(), token, refreshToken)
}

// persistLogin validates the freshly-issued access token, saves it to the

Mcmd/entire/cli/login.go+22/-18

```go
301 unmodified lines
```

// fakeBrowserFlow implements auth.BrowserAuthFlow for unit tests.
type fakeBrowserFlow struct { 
    authURL     string
    waitCode    string
}

// fakeBrowserClient implements browserAuthClient for unit tests.
type fakeBrowserClient struct {
    flow     *fakeBrowserFlow
    startErr error
}

func (c *fakeBrowserClient) StartBrowserAuth(context.Context) (auth.BrowserAuthFlow, error) {
    if c.startErr != nil {
        return nil, c.startErr
    }
    return c.flow, nil
}

func (c *fakeBrowserClient) BaseURL() string { return "http://test" }

func TestShouldUseBrowserLogin(t *testing.T) {
    t.Parallel()
}
