feat(cli): auto-rotate access_token on 401/403 with cross-process flock#2951
feat(cli): auto-rotate access_token on 401/403 with cross-process flock#2951
Conversation
Adds transparent access_token refresh so users no longer need to re-run
`profile login` just because their access_token aged out — only when the
refresh_token itself becomes invalid.
Two trigger paths in the new refreshingTransport (cli/pkg/cmdutil/factory.go):
- Reactive (replayable bodies): on a 401/403, /api/refresh and retry
the original request once with the new token. Covers every JSON
request, files cat/download/rm, and all market verbs.
- Pro-active (non-replayable bodies): for files upload chunks backed
by *os.File, decode the JWT exp before each send and rotate when
within 60s of expiry, so a streaming body is never consumed by a
request the server is about to 401.
Concurrency is handled by a new credential.Refresher (cli/pkg/credential/
refresher.go) using sync.Mutex + a gofrs/flock-backed cross-process lock
(cli/internal/lockfile) plus double-check before and after each gate, so
across goroutines AND parallel olares-cli processes /api/refresh is hit
at most once per stale token.
When refresh itself fails:
- 401/403 from /api/refresh stamps InvalidatedAt on the keychain
entry and surfaces *credential.ErrTokenInvalidated; subsequent
commands skip the network round-trip and go straight to the
"run profile login" CTA.
- Reformat helpers in cmd/ctl/files and cmd/ctl/market detect the
typed credential errors via errors.As and surface them clean,
avoiding the *url.Error "Get \"https://...\":" wrapping.
Removes manual X-Authorization injection from MarketClient, download.Client,
upload.Client, and rm.Client — they now just consume the factory-provided
http.Client. Also drops the early ErrTokenExpired short-circuit in
DefaultProvider.Resolve so the transport gets to refresh stale JWTs
instead of failing fast.
Tested with goroutine + child-process concurrency (TestRefresh_Concurrent
Goroutines, TestRefresh_CrossProcess) and the full transport matrix
(401, 403, second-401-no-loop, non-replayable body, refresh failure,
preflight stale/within-skew/fresh/replayable/failure).
Skills (olares-shared/files/market) bumped to 1.1.0 with new
"Automatic token refresh" sections.
Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0b7effe. Configure here.
| /files/ | ||
| /market/ | ||
| files/ | ||
| market/ |
There was a problem hiding this comment.
Unanchored gitignore patterns will hide new source files
High Severity
Changing /files/ and /market/ (root-anchored) to files/ and market/ (unanchored) makes the pattern match directories named files or market at any depth. This silently gitignores new files added to tracked source directories like cli/cmd/ctl/files/, cli/internal/files/, and cli/cmd/ctl/market/. Already-tracked files are unaffected, but any newly created source file in those directories will be invisible to git status / git add ., requiring git add -f to include it.
Reviewed by Cursor Bugbot for commit 0b7effe. Configure here.


Summary
Adds transparent
access_tokenrefresh toolares-cliso a single command no longer aborts when the access token aged out — users only need to runprofile loginwhen the refresh_token itself becomes invalid. Token rotation is also de-duplicated across goroutines and across concurrentolares-cliprocesses.What changed
Core (new behaviour)
refreshingTransportincli/pkg/cmdutil/factory.gois now theRoundTripperbehind every factory-provided*http.Client. Two trigger paths:401/403, call/api/refresh, then retry the original request once with the new token. Covers every JSON /files cat/files download/files rm/marketverb.files uploadchunks backed by*os.File, decode the JWTexpbefore each send and rotate when within60sof expiry. Required because a consumed stream cannot be replayed on a 401.credential.Refresherincli/pkg/credential/refresher.goprovides the actual rotation primitive: in-processsync.Mutex+ cross-processgofrs/flocklock (under<config-dir>/locks/<sanitized-olaresId>.refresh.lock) + double-check before and after each gate, so/api/refreshis hit at most once per stale token even when many goroutines / parallel CLI processes race the same expiry window.internal/lockfile(new package) wrapsgofrs/flockwith context-cancelable acquisition and a deterministic per-olaresId path layout.auth.ErrRefreshUnauthorized— new sentinel;auth.Refreshwraps401/403from/api/refreshwith it so the refresher can distinguish a dead grant from a transient 5xx / network blip.Failure handling
/api/refreshreturning401/403stampsInvalidatedAton the keychain entry and surfaces*credential.ErrTokenInvalidated(profile listalready shows these asinvalidated). Subsequent commands skip the network round-trip and go straight to the CTA.reformatHTTPErr,reformatRmHTTPErr,MarketClient.executeRequest) detect*credential.ErrTokenInvalidated/*credential.ErrNotLoggedInviaerrors.Asand surface them verbatim. Eliminates the previousrequest failed: Get \"https://...\": refresh token for X became invalid...double-wrapping; users now see just the typed error message with its built-inrun profile loginCTA.MarkInvalidatedfailure no longer masks the typed error — it is logged to stderr instead, and the caller still receives*ErrTokenInvalidatedso the CTA renders.Cleanup at call sites
X-Authorizationinjection fromMarketClient,download.Client,upload.Client, andrm.Client. They now just consume the factory-provided*http.Client. TheMarketClientkeeps two clients (timed + untimed) but they share the samerefreshingTransportso refreshes are immediately visible to both.ErrTokenExpiredexit incredential.DefaultProvider.Resolveso the transport gets to refresh stale JWTs instead of failing fast on the local heuristic.Skills
Bumped
olares-shared,olares-files,olares-marketfrom1.0.0→1.1.0and added an "Automatic token refresh" section toolares-shared(canonical) plus targeted updates inolares-files(call out the streamingfiles uploadpre-flight path) andolares-market(update the auth-transport section, drop the stalenewMarketUploadHTTPClientreference).Misc
.gitignore: switch/files/,/market/patterns from repo-root-only to anywhere, adduser-service/. Pre-existing local maintenance, folded in by the user's request.Test plan
go build ./...go test -race -count=1 ./pkg/cmdutil/... ./pkg/credential/... ./internal/lockfile/...— full concurrency + transport matrixgo test -count=1 ./cmd/ctl/... ./internal/files/...— unit tests for the touched call sitesTestRefresh_ConcurrentGoroutines— 50 goroutines on the same Refresher → exactly 1 hit on/api/refresh.TestRefresh_CrossProcess— re-execs the test binary 4× sharing a file-backed token store +OLARES_CLI_HOME→ still exactly 1/api/refreshhit, validates theflock+ double-check works across processes.cli/pkg/cmdutil/factory_test.go):X-Authorization.files uploadof a multi-chunk file across the 60s expiry boundary, andmarket install --watchafter letting the token age out).Made with Cursor
Note
High Risk
High risk because it changes the CLI-wide HTTP/auth transport behavior (automatic refresh + retry on 401/403) and adds cross-process locking/persistence logic that affects all networked commands.
Overview
Introduces transparent access-token rotation across the CLI.
cmdutil.Factorynow provides HTTP clients backed by a newrefreshingTransportthat injectsX-Authorization, refreshes the token on 401/403, and retries eligible requests once; a no-timeout variant is added for long uploads.Adds a new cross-process refresh primitive. New
credential.Refresherperforms/api/refreshwith in-process mutex + per-userflock-based locking (internal/lockfile) and stampsInvalidatedAton refresh-token rejection so subsequent commands fail fast with typedErrTokenInvalidated/ErrNotLoggedIn.Migrates call sites to rely on the transport. Files (
cat/download/upload/rm) and market clients drop manualX-Authorizationhandling, update error reformatting to surface typed credential errors cleanly, and tests are updated/added to cover retry/preflight behavior and concurrency; docs/skills are bumped to describe the new auto-refresh behavior.Reviewed by Cursor Bugbot for commit 0b7effe. Bugbot is set up for automated code reviews on this repo. Configure here.