Repository navigation
[TW-6922] fix(cli): address remaining OAuth review items (#14, #15, #21, #22, #24, N11) - #126
Conversation
…N11) instead of being read as "not an OAuth session", so refresh no longer calls account.Refresh on an OAuth session, a failed read no longer counts as "no dashboard login session", and `orgs switch` reports it. replacing it. `oauth logout` checks the dashboard session's origin before it builds the DPoP service, so it never creates a key when there is no OAuth dashboard session. main.go calls oauth.WireDashboard explicitly. --quiet through common.GetOutputWriter. Structured output never carries a token. and the oauthas and filelock adapters. N11: the "kept your dashboard login session" message says to run `nylas dashboard logout` if that session has expired. Oversized test files touched by this PR are split under 600 lines with no test changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7f74070 to
1f95115
Compare
Review of PR #126 (Claude + Codex)Two reviewers examined this PR: Claude and Codex. We did a check of each finding against the code on Result: changes are necessary before merge. Two items break Summary of the claims
Findings that block the merge1. [Major] The device-code prompt always goes to stdout. (Claude + Codex)
2. [Major]
3. [Major] A new test fails when SSH environment variables are set. (Claude + Codex)
Findings that we recommend you fix4. [Major] A bad DPoP seed now stops all dashboard commands, and the error gives no help. (Claude)
5. [Minor]
6. [Minor]
7. [Minor] Some new branches have no tests. (Claude)
8. [Minor] Some documents are not correct or not complete. (Claude)
Nits (optional)
Build and test results (
|
| Check | Result |
|---|---|
go build ./... |
Pass |
go vet ./... |
Pass |
go vet -tags integration ./internal/cli/oauth/ |
Fail (finding 2) |
golangci-lint on the changed packages |
0 issues |
go test on the changed packages, SSH variables set |
Fail (finding 3) |
go test -race -count=1 on the changed packages, SSH variables not set |
Pass |
The test file splits are correct. We compared all top-level declarations: adapters/dashboard 28/28, adapters/mcp 126/126, app/auth 69/69, app/dashboard 84/84 (plus 5 new ones).
🤖 Generated with Claude Code
- `oauth login --device` writes the verification URL and code to stderr under --json/--format yaml/--quiet, so stdout carries only the report; --quiet keeps the prompt because the login cannot finish without it. - login_kept_session_test.go gets the !integration build tag, fixing `go vet -tags integration` and make ci-full. - The JSON login test clears SSH_* variables so it passes in SSH shells. - A stored DPoP key that is not valid is reported as ErrDashboardDPoPKeyInvalid with a recovery hint; a failed read as ErrDashboardDPoPKeyUnreadable. `nylas dashboard logout` removes an invalid key and its session locally; the next login creates a new key. - `nylas oauth logout` still clears an OAuth dashboard session when the DPoP key cannot be loaded, and reports it was not revoked. - `dashboard logout` on a session whose server cannot be checked clears it and reports "cleared but not revoked" instead of returning nil. - Docs: device prompt output, DPoP key handling and recovery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review of PR #126 (Claude + Codex), commit
|
| Item | Status | Evidence |
|---|---|---|
| 1. Device prompt goes to stdout | Fixed | login.go:104-117 writes the prompt to stderr in structured mode and with --quiet. This applies to --device, --no-browser and the SSH path. The fake now calls OnDeviceAuthorization (oauth_test.go:46-52). We removed the stderr switch, and the new test (output_test.go:212-245) failed. Thus the test finds the problem. |
2. Build tag on login_kept_session_test.go |
Fixed | go vet -tags integration ./... passes. |
| 3. Test fails when SSH variables are set | Fixed | clearSSHEnv (oauth_test.go:515). The tests pass with and without the SSH variables. |
| 4. Bad DPoP seed stops all dashboard commands | Fixed, with a new small problem | The error now gives a recovery instruction (dpop.go:31). ErrDashboardDPoPKeyInvalid and ErrDashboardDPoPKeyUnreadable are different errors (errors.go:73-78). dashboard logout removes the bad key. Refer to new finding 1. |
5. oauth logout keeps the dashboard session when the seed is bad |
Fixed | helpers.go:167-173 calls ForgetIfOAuthThen. This removes the session under the lock. The message is not correct. Refer to new finding 2. |
6. dashboard logout skips the revocation without a message |
Fixed | auth_service.go:148-154 returns "cleared but not revoked". Test: forget_session_test.go:57-74. |
| 7. New branches have no tests | Partly fixed | Now tested: the device prompt with --json and --quiet, and the read error of the legacy server. Not tested: orgs switch when the origin read fails, thenUnlessOAuthSession with ran=false, --format yaml, and the new recovery branch (new finding 4). |
| 8. Documents | Fixed | COMMANDS.md:189-194,291-297 and security/overview.md:28 agree with the code. One message does not agree. Refer to new finding 2. |
| Nits | Not fixed | All seven nits from the first review stay. They are optional. |
We also did a check of these points:
- A transient keyring read error does not delete the key. Only a value that the CLI read correctly, and that does not decode, is "invalid". A read failure is "unreadable", and the CLI deletes nothing.
errors.Isfinds both new errors through all callers. The%wanderrors.Joinwrapping is correct.dashboard logoutwith a bad key removes the legacy session or the OAuth-origin session. It keeps thenylas oauth logintokens. This is correct, because both sessions use the bad key.
New findings
1. [Minor] The recovery can delete a valid key and an active session. (Claude + Codex, reproduced by Codex)
- Files:
internal/cli/dashboard/logout.go:20-23,internal/app/dashboard/oauth_session.go:256-265 - The CLI examines the key without the lock (
helpers.go:54-55). ThenForgetSessiongets the lock and deletes the key. It does not examine the key again. - Failure sequence:
- Process A finds the bad key.
- Before A gets the lock, a different process removes the key, and a new login stores a valid key and session.
- Process A gets the lock. It deletes the valid key and the new session.
- Result: the new session stays active on the server, and nothing can revoke it. TW-5023: fix: correct Nylas dashboard URL typo #15 must prevent this type of orphaned session. The time window is small, but the result is permanent.
- Codex reproduced this with a scratch test.
- Fix:
- In
ForgetSession, read the key again under the lock. - Delete the key only if it is still not valid.
- If the key is now valid, use the usual logout with revocation.
- If the read fails, keep the key and return the error.
- In
2. [Minor] The oauth logout message is not correct when the DPoP key cannot load. (Claude, code reading)
- Files:
internal/cli/oauth/logout.go:41-44,internal/cli/dashboard/helpers.go:172 - The user sees: "Could not end the dashboard session: the dashboard session was not revoked on the server: ...".
- But the CLI removed the session locally. Thus "Could not end" is not correct. Also, the docs say that the CLI removes the session.
- The CLI also shows "not revoked" when it finds no OAuth session under the lock. In this case, it removed nothing.
- Fix:
- Change the message to "The dashboard session was removed locally, but not revoked on the server".
- Show this message only when the CLI removed a session.
3. [Minor] The CLI replaces an empty stored seed without a message. (Codex, reproduced)
- Files:
internal/adapters/dpop/dpop.go:46,internal/cli/dashboard/helpers.go:54-55 - Condition: the store contains
dashboard_dpop_keywith an empty value, andGetreturns no error. - Then
seedB64 != ""is false, andNewmakes and stores a new key. This is the silent replacement that TW-5023: fix: correct Nylas dashboard URL typo #15 removes. - Also,
loadOrCreateDPoPKeythinks that a key is present. Thus it does not get the creation lock. - Fix:
- Make a new key only when
errors.Is(err, domain.ErrSecretNotFound). - Return
ErrDashboardDPoPKeyInvalidfor an empty value that the CLI read correctly.
- Make a new key only when
4. [Minor] The recovery branch of dashboard logout has no test. (Claude)
- File:
internal/cli/dashboard/logout.go:21-23,47-60 - If a person removes the
ErrDashboardDPoPKeyInvalidbranch, all tests still pass. This branch is the only recovery path in the docs. - Fix: add two tests.
- An invalid key: make sure that the key and the session are removed, and that the OAuth tokens stay.
- An unreadable key: make sure that the CLI deletes nothing.
Nits (optional)
- The recovery messages ignore the output flags.
logoutWithInvalidKey(logout.go:57-58) writes the warning to stdout withcommon.Yellow.Println. Thus--jsonand--format yamlgive text, and--quietdoes not hide the success message. (Codex, reproduced.) The usualdashboard logoutpath has the same behavior before this PR. We recommend that you usecommon.PrintWarningStderrfor the warning, at minimum. - The recovery message can be incorrect. It says "the session was removed" also when no session was stored. (Claude, reproduced.)
- A revocation can stop without a good reason.
auth_service.go:148also skips the revocation when only the write of the session server fails (session_store.go:79-81). In this case, the server check passed before. (Claude, code reading only. This is rare.)
Build and test results (222a571)
| Check | Result |
|---|---|
go build ./... |
Pass |
go vet ./... |
Pass |
go vet -tags integration ./... |
Pass (failed on 1f95115) |
golangci-lint run ./internal/... ./cmd/... |
0 issues |
go test -race -count=1 on the changed packages, SSH variables set |
Pass (failed on 1f95115) |
go test -race -count=1 on the changed packages, SSH variables not set |
Pass |
| File sizes | All changed Go files have fewer than 600 lines. The largest is account_client_test.go with 576 lines. docs/COMMANDS.md has 1,026 lines, and it had 1,006 lines on main before this PR. |
Both reviewers used scratch tests for the reproductions. We deleted the scratch tests after use.
🤖 Generated with Claude Code
Resolves conflicts with #126 (structured --json/--format yaml/--quiet output): - oauth login: keep loginOptionsFor (--mcp-only) and writeLoginReport. The structured path now skips the dashboard exchange for an MCP-only credential, as the text path does, and reports it under mcp_credential. - oauth status: structured output covers MCP-only credentials (mcp_credentials, --credential) and adds access, token_claims.org and token_claims.principal. - Tests for both; COMMANDS.md documents the new fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why this PR exists
These are the last review fixes from the TW-6922 CLI OAuth work. The commit (
5ce9a94, 2026-10-08) was made ontw-6922-cli-oauthafter #123 had been squash-merged, so it never reachedmain. GitHub had already auto-deleted the remote branch, which left the local branch as its only copy. This PR cherry-picks it onto currentmainunchanged; it applied with no conflicts.What it changes
isOAuthSessionreturns(bool, error). A keyring read error is now reported instead of being treated as "not an OAuth session". Sorefreshno longer callsaccount.Refreshon an OAuth session, a failed read no longer counts as "no dashboard login session", andorgs switchreports the error.dpop.Newreports a malformed or wrong-length stored seed instead of silently replacing it.oauth logoutchecks the dashboard session's origin before building the DPoP service, so it never creates a key when there is no OAuth dashboard session.init().main.gocallsoauth.WireDashboard()explicitly.oauth status,loginandlogouthonour--json,--format yamland--quietthroughcommon.GetOutputWriter. Structured output never carries a token, and in structured mode the browser prompt goes to stderr.ARCHITECTURE.mdlists the new ports (oauthas,lock, MCP credentials) and adapters.Verification
On this branch rebased onto
main:go build,go vet,make lint(0 issues) andgo test -race ./...all pass.There's one small hunk in
internal/cli/oauth/login.go, around the pre-login prompt. Whichever PR merges second needs a rebase. When resolving it, apply #22's rule to the device prompt too: in structured output (--json/--format yaml), the device code and verification URL go to stderr so stdout carries only the report, and--quietmust not hide the code, since the user can't sign in without it.🤖 Generated with Claude Code