Closes#1665
`--use-stored-auth` previously injected the stored access token blindly, so an
expired token failed even with a valid refresh_token on disk. Now, when a
`refresh_token` is stored, the CLI runs the OAuth refresh grant and injects the
fresh access token, persisting the rotation.
- New exported `refreshStoredAuthToken(serverUrl, statePath, deps?)`: reuses the
SDK's `refreshAuthorization` (not a hand-rolled token request) with the stored
`clientInformation` + `serverMetadata` (discovered via core
`getAuthorizationServerUrl` + SDK `discoverAuthorizationServerMetadata` when
the metadata wasn't persisted). Rotated tokens are written back through
`serializeOAuthPersistBlob` under the same `{servers,idpSessions}` key, so web
and CLI stay consistent. Missing refresh_token/clientInformation or a failed
grant throw `CliExitCodeError(AUTH_REQUIRED)` with a clear message + envelope
code (`no_stored_token` / `refresh_failed`).
- `--use-stored-auth` prefers refresh when a refresh_token is present (covers
both absent and expired access tokens — the stored blob has no expiry, so the
refresh token is the durable credential); falls back to injecting the stored
access token otherwise (unchanged).
- `deps` are injectable so the refresh/discovery are unit-tested without a live
token endpoint; an e2e test drives the real SDK path against a mock token
endpoint and asserts the refreshed token is injected + the rotation persisted.
- CLI README updated (removed the "blind injection" caveat).
CLI coverage gate green (cli.ts 100/92/100/100).
Part of the #1579 decomposition (Wave 4, CLI lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw