Repository navigation
feat(auth): report login_expired as the credential problem - #72
Conversation
When the auth service answers a saved login's refresh or organization token exchange with invalid_grant, the error's --json detail now carries "credential_problem": "login_expired", so agents can detect a dead login without parsing the message. voltage context stays unchanged. It reads no saved secret and makes no network request, so it cannot see that the auth service rejects a login, and an expired access token alone is refreshed by the next command. The troubleshooting and login guides say so. Closes #49
e82e229 to
53f8918
Compare
aff3faf to
4ec69d2
Compare
53f8918 to
b0e4d1b
Compare
4ec69d2 to
f6a608d
Compare
tee8z
left a comment
There was a problem hiding this comment.
Approved b0e4d1b against fix/rustsec-webpki (#71 f6a608d).
expired_login is the only source of ErrorDetail::Credential { credential_problem: login_expired }. Both invalid_grant paths, the refresh and the token exchange, reach it, and the integration test asserts the JSON detail for each. Keeping CredentialRejection separate from config::CredentialProblem is the right boundary, because voltage context can never produce it. The detail has no data, so redact correctly skips it. The rebase onto #67 keeps #67's hint, voltage login --account NAME. No link to the old #the-saved-login-has-expired-or-was-revoked anchor remains.
Nonblocking:
- In the description, the example JSON
hintstill reads "Remove it withvoltage logout --local --account NAME, then ...". On this stack, the hint isSign in again withvoltage login --account NAME. - No test pins the human
details:line,credential_problem login_expired, for this error. The JSON form is pinned.
Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (118 unit, 103 integration) and python3 scripts/check-coverage.py passed. CRAP: 555 measured functions, none at or above 20, highest wait_for_payment at 19.60; 152 functions were unmeasured. CI is green.
When the auth service answers a saved login's refresh or organization token exchange with invalid_grant, the error's --json detail now carries "credential_problem": "login_expired", so agents can detect a dead login without parsing the message. voltage context stays unchanged. It reads no saved secret and makes no network request, so it cannot see that the auth service rejects a login, and an expired access token alone is refreshed by the next command. The troubleshooting and login guides say so. Closes #49
* fix(deps): reject the distrusted emSign intermediates Update rustls-webpki to 0.103.16 and webpki-root-certs to 1.0.10. Both reject certificate paths through the emSign intermediates whose private keys are now public (RUSTSEC-2026-0382, RUSTSEC-2026-0383). Closes #68 Closes #69 * feat(auth): report login_expired as the credential problem (#72) When the auth service answers a saved login's refresh or organization token exchange with invalid_grant, the error's --json detail now carries "credential_problem": "login_expired", so agents can detect a dead login without parsing the message. voltage context stays unchanged. It reads no saved secret and makes no network request, so it cannot see that the auth service rejects a login, and an expired access token alone is refreshed by the next command. The troubleshooting and login guides say so. Closes #49
…66) * feat(cli): name VOLTAGE_API_KEY and the organization source on a 403 VOLTAGE_API_KEY takes precedence over a saved login without saying so, so a key from another organization failed with a bare 403. A 403 to that key now names it and where the organization came from (--org or VOLTAGE_ORGANIZATION_ID), and suggests --account NAME or unsetting the variable. The JSON error reports both as detail.credential_sources. Closes #47 * feat(cli): replace a saved login when the same user signs in again (#67) `voltage login` under a saved name now replaces that login when the new session is for the same user at the same auth URL, under the credential lock, and revokes the replaced refresh token. A different user, or a saved login without a user ID, is still refused, and the new session is revoked. With `--account NAME`, a name saved for an API key, a login at another auth URL, or a login whose secret cannot be read is refused before the browser step. An unreadable secret is refused as a name in use with a `voltage logout --local --account NAME` hint, and `logout --local` removes an account whose secret is already gone, or warns when the secret cannot be removed. The expired-login hint is now only `voltage login --account NAME`. Without `--credential-store`, a replaced login keeps its store and a new login uses the keychain. Any failed secret or configuration write puts the replaced login back, including a write that already renamed its file, and the error says when the replaced login could not be restored. A Ctrl-C while the replaced session is revoked says the new login is saved. `shell_word` moves to `config`, which no longer imports `auth`. Closes #48 * fix(deps): reject the distrusted emSign intermediates (#71) * fix(deps): reject the distrusted emSign intermediates Update rustls-webpki to 0.103.16 and webpki-root-certs to 1.0.10. Both reject certificate paths through the emSign intermediates whose private keys are now public (RUSTSEC-2026-0382, RUSTSEC-2026-0383). Closes #68 Closes #69 * feat(auth): report login_expired as the credential problem (#72) When the auth service answers a saved login's refresh or organization token exchange with invalid_grant, the error's --json detail now carries "credential_problem": "login_expired", so agents can detect a dead login without parsing the message. voltage context stays unchanged. It reads no saved secret and makes no network request, so it cannot see that the auth service rejects a login, and an expired access token alone is refreshed by the next command. The troubleshooting and login guides say so. Closes #49
Problem
When a saved login's refresh token is dead, the auth service answers its refresh or the organization token exchange with
invalid_grant. The CLI already names the fix in the message and hint (#24), but--jsonoutput gave agents no stable code for it, so an agent could only parse the message.Change
The
invalid_granterror now carries a typed detail:{"error": {"message": "The saved login for NAME has expired or was revoked", "exit_code": 3, "hint": "Remove it with `voltage logout --local --account NAME`, then ...", "detail": {"credential_problem": "login_expired"}}}ErrorDetail::Credential { credential_problem: CredentialRejection }insrc/error.rs, set only byexpired_logininsrc/auth.rs.CredentialRejectionis separate fromconfig::CredentialProblemon purpose.voltage contextreportsCredentialProblemand can never producelogin_expired, so the types enforce that boundary.details:line,credential_problem login_expired, under the existing message and hint.voltage contextis unchanged (decision)src/context.rsdocuments thatcontextnever reads a saved secret or makes a network request. The access-token expiry (expires_at) is stored in the secret, and only the auth service knows the refresh token is dead. An expired access token is also not a dead login, because the next command refreshes it. Socontextcannot detectlogin_expired. The troubleshooting and login guides now say so and point tovoltage auth statusand to the next command's error. Augustus approved this approach, so thecontextitem in #49's "Left" list is closed by decision. The in-place replacement item is done by #67.Closes #49
Overlap with #66 and #67
#66 and #67 also change
expired_loginandErrorDetail. This PR is now stacked on #67, and the conflicts are resolved:expired_logincarries thelogin_expireddetail and keeps #67's hint,voltage login --account NAME, and the troubleshooting entry combines both. Gate and CRAP pass on the stack tip (118 unit, 103 integration; 0 of 555 functions at or above 20).Stack
One stack, merged bottom up:
master)login_expiredcredential problem, Hint to log in again when a saved login returns invalid_grant #49Checks
nix develop:cargo fmt --check,cargo clippy --locked --all-targets -- -D warnings,cargo machete --with-metadata,cargo test --locked,python3 scripts/check-coverage.py. All pass.a_revoked_login_names_the_command_that_renews_itasserts the JSONdetailfor both the refresh and the token-exchange rejections.expired_logins_name_commands_that_can_be_pastedchecks the typed detail.details_keep_their_documented_shapespins the serialized shape.troubleshooting_covers_every_exit_code_and_credential_problemrequires alogin_expiredentry in the troubleshooting guide.voltage-pr-reviewand its Rust style checklist (errors topic: a stable code, not parsed text).scripts/crap-report.shon this head): 0 violations. Highest: 19.60,wait_for_payment(src/api.rs:1113), which this PR does not change. No function is at or above 20.