Repository navigation
feat(cli): autorun login for authed commands - #118
Conversation
filipmyllari
left a comment
There was a problem hiding this comment.
Review notes (AI-assisted). Inline comments below, plus one that doesn't map to diff lines:
- Minor —
packages/quickstart/__tests__/e2e/stale-auth.e2e.ts:114: this comment still mentionsvalidateToken, which this PR removes.
| const result = await authenticate({ | ||
| mode: 'login', | ||
| profile, | ||
| onUrl: (url) => message(`If the browser did not open, visit:\n${url}`), | ||
| }); |
There was a problem hiding this comment.
authenticate() is called with no signal and no timeout. If the user closes the browser tab or never finishes the login, any withAuth command hangs forever with the callback server still listening. Before this PR the command just failed fast. Pass an AbortSignal.timeout(...) and say in the message that Ctrl+C cancels.
There was a problem hiding this comment.
Added a default argument in authenticate with a signal
| try { | ||
| const refreshed = await refreshAccessToken(profile); | ||
| return { token: refreshed.accessToken, expired: false }; | ||
| } catch { | ||
| return { token: null, expired: true }; | ||
| } |
There was a problem hiding this comment.
Every refresh failure counts as "expired", including network errors and timeouts. On flaky wifi or a VPN drop, a command that has a perfectly good refresh token opens the browser for a full login, which then probably fails too. Only fall through to login on an auth rejection (a 4xx, or no refresh token stored). For anything else, fail with the error.
There was a problem hiding this comment.
Added two separate new cases for expired and malformed token; otherwise, re-throws
| } catch { | ||
| fail('Login failed. Try running "confidence login" to re-authenticate.'); |
There was a problem hiding this comment.
The catch drops the real error. Port-in-use, a state mismatch and a token-exchange failure all show up as "Login failed. Try running confidence login", which is the same flow that just failed. Include err.message.
|
|
||
| const token = loadPersistedToken(profile); | ||
| if (!token) { | ||
| fail('Not logged in. Run "confidence login" first.'); | ||
| return; | ||
| } | ||
|
|
||
| const validation = validateToken(token); | ||
| if (!validation.valid) { | ||
| fail('Token is expired. Run "confidence login" to re-authenticate.'); | ||
| return; | ||
| } | ||
| const token = await tryResolveAuth(profile); |
There was a problem hiding this comment.
whoami used to be a read-only status check. Now an expired or missing token triggers a refresh (which writes credentials) or opens a browser login. People and scripts use whoami to ask "am I logged in?", so it should probably keep using tryLoadAuthToken and report the status without starting a login.
There was a problem hiding this comment.
At first I thought I would disagree, but the longer I think about, the more I'm agreeing with this 😃
| message( | ||
| loaded.expired | ||
| ? 'Session expired. Opening browser to re-authenticate...' | ||
| : 'Not logged in to Confidence. Opening browser to authenticate...', | ||
| ); | ||
|
|
||
| const result = await authenticate({ | ||
| mode: 'login', | ||
| profile, | ||
| onUrl: (url) => message(`If the browser did not open, visit:\n${url}`), | ||
| }); | ||
|
|
||
| message(`Authenticated as ${result.workspace ?? 'unknown'} (${result.region})`); |
There was a problem hiding this comment.
The progress messages go through message(), which writes to stdout. The isInteractive() change protects pipes, but confidence flags list --output json run in a terminal still gets "Session expired… / Authenticated as…" mixed into its machine-readable output. Write these with error() (stderr) instead.
There was a problem hiding this comment.
Added another gated helper for interactive-focused messages, hint
| if (valid) return existing; | ||
| } | ||
| const { token } = await tryLoadAuthToken(opts?.profile); | ||
| if (token) return token; |
There was a problem hiding this comment.
resolveAuthToken now refreshes silently before falling back to authenticate. That's fine, but forceNew is the only way to skip it now. Check that the MCP setup callers expect the refresh to write to the credentials file.
| fail('Not logged in. Run "confidence login" first.'); | ||
| return null; | ||
| if (!token) return { token: null, expired: false }; | ||
| if (isValid(token)) return { token, expired: false }; |
There was a problem hiding this comment.
Minor: a malformed token fails isValid, then the refresh fails, and the user is told "Token expired" or "Session expired", which is misleading. Treat an undecodable token the same as no token.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transient failures (timeout, DNS, VPN drop) now propagate instead of being swallowed as "expired". A hasExpiredSession helper checks the error cause so tryLoadAuthToken only triggers the browser login flow when the refresh token is genuinely invalid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… output
- whoami is now read-only: uses tryLoadAuthToken instead of
tryResolveAuth so it never opens a browser login
- decodeJwtPayload throws with { cause: MALFORMED_TOKEN_ERR } so
corrupt tokens are treated as "not logged in", not "expired"
- authenticate() defaults to a 120s AbortSignal timeout so callers
can't accidentally hang
- Add hint() helper that writes to stdout only in interactive mode,
replacing message() for auth progress so piped/JSON output stays clean
- MCP resolveAuthToken catches tryLoadAuthToken throws and adds timeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks, @filipmyllari! Round two? 🙃 |
Summary
confidence flags list) without being logged in, the CLI now automatically opens the browser for OAuth instead of just printing an error. In non-interactive environments (CI, piped), the existing error messages are preserved.validateTokeninto focused functions (isValidToken,extractWorkspace,extractRegion,extractOrganization) so consumers only use what they need.Details
New three-tier token resolution in
packages/cli/src/utils/auth.ts:tryLoadTokenis the shared helper used by bothwithAuth()(general API commands, gated onisInteractive()) andresolveAuthToken(inherently interactive commands like MCP install/migrate, always attempts browser auth).Test plan
auth.test.tswith 8 tests covering all resolution paths (valid, refresh, interactive login, non-interactive fallback, failure, profile forwarding)pnpm qaclean (typecheck + lint + test)confidence flags listwithout login → opens browser, completes command after authconfidence flags list | catwithout login → shows "Not logged in" error