Skip to content

feat(cli): auto-refresh on expiry + retry-once on 401#34

Merged
I-am-nothing merged 1 commit into
mainfrom
feat/cli-refresh-on-401
Jun 6, 2026
Merged

feat(cli): auto-refresh on expiry + retry-once on 401#34
I-am-nothing merged 1 commit into
mainfrom
feat/cli-refresh-on-401

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

Makes the CLI self-heal an expired/stale access token instead of forcing mirrorstack login — symmetric with the web client's reactive refresh, and the follow-up flagged when we restored the 15-min access-token TTL (api-platform #180).

What

  • refresh_and_save(refresh_token) — exchange the stored refresh token for a rotated pair, persist it (expires_at re-derived to now+15min, matching the platform TokenConfig; save is best-effort/non-fatal).
  • with_refresh_retry(creds, call) — run the authed call; on ApiError::Unauthenticated do exactly one refresh_and_save + one retry, writing rotated creds back via &mut. Non-401 passes through; if the refresh itself 401s, the original Unauthenticated returns so the terminal session_expired() still fires. At most one retry — no loop.
  • load_or_login_hint() now also refreshes proactively on expirymain's version didn't check expiry at all (that fix lived only on the feat/monorepo-cli branch), so this also closes that gap on main.

Wired through all 6 authed call sites (whoami; module me/get/create/refetch; dev tunnel-token — replacing its hand-rolled refresh-and-retry).

Verification

  • cargo build + cargo test (67 pass, incl. 2 new) + cargo clippy + cargo fmt --check all clean.
  • Adversarial Rust review: approve, no must-fixes — confirmed retry-once (no loop), behavior preserved (refresh-failure → session_expired, non-401 unchanged), borrow soundness, no token logging.

Branches off main (clean — no backlog stacking).

🤖 Generated with Claude Code

Make the CLI self-heal an expired/stale access token instead of forcing
`mirrorstack login`, symmetric with the web client's reactive refresh.

- refresh_and_save(refresh_token): exchange the stored refresh token for a
  rotated access+refresh pair and persist it (expires_at re-derived locally to
  now+15min, matching the platform's TokenConfig; save is best-effort).
- with_refresh_retry(creds, call): run the authed call; on ApiError::
  Unauthenticated do exactly ONE refresh_and_save + ONE retry, writing the
  rotated creds back through &mut Credentials. Non-401 errors pass through; if
  the refresh itself 401s (refresh token gone) the original Unauthenticated is
  returned so the caller's terminal session_expired()/login-hint still fires.
  At most one retry — no loop.
- load_or_login_hint() now also refreshes proactively when the loaded token is
  past expiry (main's version did not check expiry at all). Best-effort:
  refresh failure falls back to the stale creds so with_refresh_retry retries.

Wired through the 6 authed call sites (whoami, module me/get/create/refetch,
dev tunnel-token — replacing dev's hand-rolled refresh-and-retry). 67 tests
pass (incl. 2 new), clippy clean, cargo fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 24780a7 into main Jun 6, 2026
5 checks passed
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.

1 participant