feat(auth): refresh an expiring OAuth token before token view / status - #123
Merged
Merged
Conversation
…atus` `ol auth token view` printed whatever access token was stored, so once an OAuth token expired (they last about an hour) it kept handing scripts a dead token until some other command happened to refresh it. Wire cli-core 1.6.0's `refresh` option on both `token view` and `status` so an expiring token is rotated first. `status` previously did this by hand via `refreshedTokenForStatus`, which re-read the store through the module singleton and swallowed every refresh error. cli-core now rotates the selected account through the attacher's own `--user`-aware store and hands `fetchLive` the live token; a rejected refresh token surfaces as `AUTH_REFRESH_EXPIRED` (re-login) instead of silently probing with the stale one.
doistbot
reviewed
Sep 16, 2026
doistbot
left a comment
Member
There was a problem hiding this comment.
This PR wires cli-core's new refresh option into both auth attachers via a shared getTokenRefreshOptions() helper, removing the hand-rolled refreshedTokenForStatus from auth status and making auth token view rotate an expiring OAuth token before printing it.
Few things worth tightening:
auth-token.test.ts's store mock lacksactiveBundle, so thetoken viewtests now throwstoreMocks.activeBundle is not a function— add the mock or stubgetTokenRefreshOptionstoundefinedthere likeauth-command.test.tsdoes.- In
auth-command.test.ts, the sentinelmockReturnValueforgetTokenRefreshOptionsleaks into every later test in the file (vi.clearAllMocks()only clears calls); switch tomockReturnValueOnceor restore withmockReturnValue(undefined)so subsequent tests exercise the stored-token path as the fixture promises.
… default explicit `auth-token.test.ts` only stayed on the stored-token path because its store stub happened to lack `setBundle`. Stub `getTokenRefreshOptions` there like the other command-surface suite does, and add a test that drives cli-core's real refresh through `ol auth token view` with a fake provider. Both suites now restore the fixture default after registering with a real refresh, since each registration calls the builder twice.
scottlovegrove
requested review from
a team and
rfgamaral
and removed request for
a team
September 16, 2026 17:11
doist-release-bot Bot
added a commit
that referenced
this pull request
Sep 16, 2026
## [2.1.0](v2.0.9...v2.1.0) (2026-09-16) ### Features * **auth:** refresh an expiring OAuth token before `token view` / `status` ([#123](#123)) ([2584eaf](2584eaf))
Contributor
|
🎉 This PR is included in version 2.1.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ol auth token viewread the access token straight from the store and never triggered a refresh. Outline access tokens last about an hour, so once one expired the command kept printing the same dead token until some other API command happened to rotate it, which defeats the point ofexport OUTLINE_API_TOKEN="$(ol auth token view)"for scripts.ol auth statusworked around the same gap with a hand-rolledrefreshedTokenForStatusinsidefetchLive.Change
Bumps
@doist/cli-coreto 1.6.0 (Doist/cli-core#135) and wires its newrefreshoption on both attachers via a sharedgetTokenRefreshOptions()insrc/lib/auth.ts. The handshake is resolved per account through the function form ({ baseUrl, clientId }from the account cli-core selected), so--user <other>refreshes the right account at the right instance without a second store read.token view: rotates an expiring OAuth token before printing. Env, legacy and manual-token accounts carry no refresh token and are served from the single bundle read, so their behaviour is unchanged. The env-var guard still runs before any refresh.status:refreshedTokenForStatusis gone. cli-core rotates through the attacher's own--user-aware store and handsfetchLivethe live token. One behavioural difference worth knowing: the old helper swallowed every refresh error and probed with the stale token, so a rejected refresh token surfaced as aNO_TOKENfrom the 401. It now surfaces as cli-core'sAUTH_REFRESH_EXPIRED(re-login required) before the probe, which is the same outcome with a clearer cause. Transient refresh failures still fall back to the stored token while it is valid.Tests
auth-command.test.ts: the cli-core attachers are now pass-through spies, and a wiring test asserts bothstatusandtoken viewreceive the samerefreshoptions. The defaultmockOutlineAuthModulefixture stubsgetTokenRefreshOptionstoundefinedso the existing command-surface tests keep exercising the stored-token path.lib/auth.test.ts:getTokenRefreshOptionsreturns the config-scoped lock path and a handshake resolver that pins to the given account's base URL and client id.Skill content and README updated for the
token viewbehaviour;SKILL.mdregenerated. The lockfile diff beyond the cli-core bump is npm 10.8 metadata drift (libc/devflags), not dependency changes.