Skip to content

feat(cli): refresh access token before expiry#44

Merged
appleboy merged 12 commits into
mainfrom
feat/proactive-token-refresh
May 24, 2026
Merged

feat(cli): refresh access token before expiry#44
appleboy merged 12 commits into
mainfrom
feat/proactive-token-refresh

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

Keep CLI users (and scripts calling authgate-cli token get) logged in by proactively refreshing the stored access token before it expires. Previously the token was only refreshed after full expiry, and token get never refreshed at all. Now, when the token falls within a configurable threshold of expiry (default 5m), it is exchanged for a fresh one and saved back.

AI Authorship

  • No AI was used in this PR
  • AI was used. Details:
    • Tool / model: Claude Opus 4.7 (1M context), via Claude Code
    • AI-authored files: all changed files — auth.go, config.go, main.go, token_cmd.go, README.md, config_test.go, token_cmd_test.go
    • Human line-by-line reviewed: ⚠️ None yet — not reviewed line-by-line at time of opening. Reviewers should treat the whole PR as needing close attention, especially the auth/token-flow paths.

Change classification

  • Leaf node (local impact)
  • Core code (broad impact — needs line-by-line review)
    • Touches the OAuth token-refresh decision used by both token get and the main run() flow. A bug here affects every authenticated invocation.

Plan reference

See plan.md (branch root). Goal: "once logged in, stay logged in" — read the access token, and if it's within N minutes of expiry (N configurable, default 5m), refresh it proactively. Done = (1) token get and run() auto-refresh near expiry; (2) threshold tunable via --refresh-threshold flag and REFRESH_THRESHOLD env; (3) graceful degradation when refresh fails but the old token is still valid.

Verification

  • Unit tests — TestNeedsRefresh (boundary cases: empty token, far-from-expiry, exactly-at-threshold, inside, expired), TestRefreshThresholdConfig (default 5m + env/flag resolution)
  • Integration tests — TestRunTokenGet_ProactiveRefresh against an httptest token endpoint with a temp-file store
  • At least 3 e2e tests (1 happy path + 2 errors):
    • Happy path: token expiring in 2m → token get refreshes, prints the new token, store ExpiresAt updated to the future
    • Error (refresh fails, token still valid): token expiring in 4m, endpoint returns 500 → exit 0, prints old token, stderr warning
    • Error (refresh token invalid + token expired): expired token, endpoint returns invalid_grant → non-zero exit, stderr re-auth hint
    • Plus: JSON output reflects refreshed expiry; token far from expiry makes no network request
  • Stress / soak test: N/A (no long-running/async code)
  • Manual verification:
    make build
    # Log in with a short-lived token, wait until near expiry, then:
    ./bin/authgate-cli token get        # should auto-refresh and print the new token
    ./bin/authgate-cli token get --json # expired=false, expires_at = new value

Verifiability check

  • Inputs and outputs are documented (needsRefresh / ensureFreshToken doc comments; README config table)
  • Reviewer can judge correctness without reading every line — behavior is pinned by the e2e tests above
  • Failures surface to the user (stderr warnings on refresh failure; non-zero exit when re-auth is required)

Security check

  • No secrets in code
  • All external inputs validated — token response goes through existing validateTokenResponse
  • Refresh token is never printed — tests assert it does not appear in stdout/stderr
  • Errors don't leak internals — reuses existing formatHTTPError / OAuth error masking; token get --json still omits the refresh token

Risk & rollback

  • Risk: token get changes from a purely local read to a possible network call. Mitigation: it only contacts the server when inside the threshold window; on failure it gracefully returns the still-valid token, so scripts aren't interrupted. Offline use beyond the threshold makes zero network requests (test-covered).
  • Risk: token get now uses loadConfig() + resolveEndpoints instead of loadStoreConfig(), so environments missing endpoint config could error during resolution. Mitigation: reuses the existing buildTokenInspectCmd resolution path and its warnings/fallbacks.
  • Rollback: revert this commit. The token.json on-disk format is unchanged, so no data migration is needed.

Reviewer guide

  • Read carefully:
    • auth.goneedsRefresh (boundary semantics: inclusive at-threshold) and ensureFreshToken (graceful-degradation branches)
    • main.gorun() now uses !needsRefresh(...) and reuses the old token on refresh failure when still valid
    • token_cmd.gorunTokenGet error handling (not-found vs ErrRefreshTokenExpired vs generic) and the switch to loadConfig() + resolveEndpoints
  • Spot-check OK:
    • config.go — mechanical addition mirroring existing timeout settings
    • README.md — docs only
    • test files — behavior is asserted; signatures + assertions suffice

- Add a refresh threshold setting with --refresh-threshold flag and REFRESH_THRESHOLD env, defaulting to 5m
- Refresh the stored access token in token get and the main flow when it expires within the threshold window
- Reuse the existing token without any network request while it is further than the threshold from expiry
- Fall back to the existing valid token with a warning when refresh fails, and only re-authenticate once expired
- Document the new threshold setting in the configuration reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 23, 2026 18:01
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 23, 2026

Codecov Report

❌ Patch coverage is 51.81818% with 53 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
main.go 0.00% 21 Missing ⚠️
token_cmd.go 53.57% 13 Missing ⚠️
config.go 61.11% 6 Missing and 1 partial ⚠️
tui/bubbletea_manager.go 0.00% 7 Missing ⚠️
tui/simple_manager.go 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the CLI’s token handling so both the main run() flow and authgate-cli token get proactively refresh a stored access token when it is within a configurable pre-expiry window (default 5 minutes), aiming to keep users/scripts continuously authenticated.

Changes:

  • Introduces needsRefresh / ensureFreshToken to decide and perform pre-expiry refresh with graceful degradation when the token is still valid.
  • Adds --refresh-threshold / REFRESH_THRESHOLD configuration plumbing and documentation.
  • Updates token get to use full config + endpoint resolution and adds unit/integration tests for proactive refresh behavior and boundary cases.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
auth.go Adds refresh-decision helper and a shared “ensure fresh token” path used by commands.
main.go Switches cached-token decision from “not expired” to “not within refresh threshold” and adds graceful reuse on refresh failure.
config.go Adds refresh-threshold flag/env/default and stores it on AppConfig.
token_cmd.go Updates token get to load full config, resolve endpoints, and refresh near expiry before printing.
config_test.go Adds tests covering refresh-threshold config precedence and default.
token_cmd_test.go Updates existing tests for new runTokenGet signature and adds proactive refresh integration tests + needsRefresh unit tests.
README.md Documents REFRESH_THRESHOLD and corresponding flag behavior.
Comments suppressed due to low confidence (1)

token_cmd.go:342

  • The JSON expired field uses time.Now().After(tok.ExpiresAt), which reports not-expired when now == ExpiresAt. For an expiry instant, equality should count as expired. Use an inclusive comparison (e.g. !time.Now().Before(tok.ExpiresAt)) so boundary behavior matches typical expiry semantics.
		out := tokenGetOutput{
			AccessToken: tok.AccessToken,
			TokenType:   tok.TokenType,
			ExpiresAt:   tok.ExpiresAt,
			Expired:     time.Now().After(tok.ExpiresAt),
			ClientID:    tok.ClientID,
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread auth.go Outdated
Comment on lines +100 to +101
// Graceful degradation: keep using the old token while it's still valid.
if time.Now().Before(tok.ExpiresAt) {
Comment thread auth.go Outdated
Comment on lines +94 to +101
if !needsRefresh(tok, cfg.RefreshThreshold, time.Now()) {
return tok, false, nil
}

newTok, err := refreshAccessToken(ctx, cfg, tok.RefreshToken)
if err != nil {
// Graceful degradation: keep using the old token while it's still valid.
if time.Now().Before(tok.ExpiresAt) {
Comment thread token_cmd.go Outdated
)
fmt.Fprintf(stderr, "Hint: run 'authgate-cli' to re-authenticate.\n")
default:
fmt.Fprintf(stderr, "Error: failed to load token: %v\n", err)
Comment thread main.go Outdated
Comment on lines 89 to 96
@@ -95,6 +95,12 @@ func run(ctx context.Context, ui tui.Manager, cfg *AppConfig) int {
newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken)
if err != nil {
- Refuse to reuse an empty access token when refresh fails, surfacing the error instead of returning an unusable token
- Capture the current time once so the refresh decision and validity check reason about the same instant
- Reword the token get fallback error so it no longer implies a load failure when refresh failed
- Treat the JSON expired field as inclusive at the expiry instant, matching the refresh boundary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment thread token_cmd.go
Comment on lines 85 to 88
RunE: func(cmd *cobra.Command, args []string) error {
cfg := loadStoreConfig()
cfg := loadConfig()
resolveEndpoints(cmd.Context(), cfg)
if code := runTokenGet(
Comment thread main.go Outdated
Comment on lines 93 to 95
@@ -95,6 +95,12 @@ func run(ctx context.Context, ui tui.Manager, cfg *AppConfig) int {
newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken)
Comment thread main.go Outdated
Comment on lines 89 to 90
if !needsRefresh(existing, cfg.RefreshThreshold, time.Now()) {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenStillValid})
Comment thread auth.go Outdated
Comment on lines +101 to +105
newTok, err := refreshAccessToken(ctx, cfg, tok.RefreshToken)
if err != nil {
// Graceful degradation: keep using the old token only while it's still
// usable — present and not yet expired. An empty access token is never
// reusable, so a refresh failure there surfaces the error rather than
Comment thread README.md Outdated
| `USERINFO_TIMEOUT` | `10s` | `/oauth/userinfo` request |
| `DISCOVERY_TIMEOUT` | `10s` | OIDC Discovery fetch |
| `REVOCATION_TIMEOUT` | `10s` | RFC 7009 revocation request issued by `token delete` |
| `REFRESH_THRESHOLD` | `5m` | Refresh the stored access token when it expires within this window; used by 'token get' and the main flow. No network request while the token is further from expiry |
Comment thread token_cmd_test.go
Comment on lines +646 to +648
if called.Load() {
t.Error("no network request expected when token is far from expiry")
}
- Defer OIDC discovery until a refresh is actually needed so token get makes no network request while the token is far from expiry
- Skip the refresh round trip entirely when no refresh token is stored, degrading gracefully when the access token is still valid
- Capture the current time once in run() so the refresh decision and reuse check share one instant
- Add tests asserting discovery is not contacted far from expiry and is resolved before refreshing near expiry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Comment thread token_cmd.go Outdated
Comment on lines +86 to +88
cfg := loadConfig()
// Endpoints are resolved lazily inside ensureFreshToken, only when a
// refresh is actually needed, so reads far from expiry stay offline.
Comment thread auth.go
Comment on lines +115 to +119
// Without a refresh token a refresh can only fail after a wasted round
// trip, so skip the network entirely and degrade gracefully.
if tok.RefreshToken == "" {
return reuseOrFail(ErrRefreshTokenExpired)
}
Comment thread README.md
| `--version` | — | Print version and exit (also available as `version` subcommand) |

Each timeout / size environment variable also has a matching flag with the same name in lower-kebab-case (e.g. `--callback-timeout`, `--max-response-body-size`).
Each timeout / size / threshold environment variable also has a matching flag with the same name in lower-kebab-case (e.g. `--callback-timeout`, `--max-response-body-size`, `--refresh-threshold`).
Comment thread main.go Outdated
Comment on lines 96 to 98
@@ -95,6 +98,12 @@ func run(ctx context.Context, ui tui.Manager, cfg *AppConfig) int {
newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken)
Comment thread token_cmd.go Outdated
case errors.Is(err, ErrRefreshTokenExpired):
fmt.Fprintf(
stderr,
"Error: stored token expired and refresh token is no longer valid: %v\n",
- Build the network-capable config lazily so token get validates SERVER_URL and constructs the HTTP client only when a refresh is actually needed, keeping far-from-expiry reads fully offline
- Add ErrNoRefreshToken so a missing refresh token is no longer reported as an expired one
- Reword the token get re-auth error to avoid asserting expiry for non-expiry cases
- Emit the expired status only for truly expired tokens, not ones merely inside the refresh window
- List --refresh-threshold in the CLI flags reference table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread main.go Outdated
Comment on lines +101 to +110
if !now.Before(existing.ExpiresAt) {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenExpired})
}
newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken)
if err != nil {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshFailed, Err: err})
// Graceful degradation: reuse the old token while it's still
// valid; only fall through to re-authentication once expired.
if now.Before(existing.ExpiresAt) {
storage = &existing
Comment thread config.go Outdated
MaxResponseBodySize int64

// RefreshThreshold is the window before expiry within which the stored
// access token is proactively refreshed. Resolved only by loadConfig.
- Mirror ensureFreshToken in the main flow: skip the doomed network call (and misleading "refresh failed" status) when no refresh token exists, reusing the still-valid token or falling through to re-auth
- Resolve RefreshThreshold in loadStoreConfig so the offline token get path and loadConfig share one source, and correct the field comment

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

main.go:120

  • In the refresh-failed branch, the graceful-degradation path reuses existing whenever now.Before(existing.ExpiresAt), even if existing.AccessToken is empty. Since needsRefresh treats an empty access token as requiring refresh, a failure here could leave storage set to an empty token and cause downstream calls (verify/userinfo) to run with an empty Authorization header. Add a non-empty access-token check (as in ensureFreshToken) before reusing the cached token.
			} else if newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken); err != nil {
				ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshFailed, Err: err})
				// Graceful degradation: reuse the old token while it's still
				// valid; only fall through to re-authentication once expired.
				if now.Before(existing.ExpiresAt) {
					storage = &existing
					flow = "cached"
				}

Comment thread main.go
Comment on lines +104 to +112
if existing.RefreshToken == "" {
// No refresh token means a refresh can only fail, so skip the
// network call (and the misleading "refresh failed" status).
// Reuse the old token while it's still valid; otherwise fall
// through to re-authentication.
if now.Before(existing.ExpiresAt) {
storage = &existing
flow = "cached"
}
- Require a non-empty access token (not just a future expiry) before reusing the existing token when refresh is skipped or fails, mirroring ensureFreshToken so a corrupt token never proceeds to fail confusingly downstream

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread main.go Outdated
Comment on lines +118 to +123
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshFailed, Err: err})
// Graceful degradation: reuse the old token while it's still
// valid; only fall through to re-authentication once expired.
if reuseValid {
storage = &existing
flow = "cached"
- Emit EventRefreshFailed only when the main flow will actually re-authenticate; when the cached token is still usable, keep using it silently instead of showing "starting new authentication flow"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread main.go Outdated
Comment on lines +97 to +103
// Only report expiry when the token is actually expired; a token
// merely inside the refresh-threshold window is still valid, so the
// "expired" message would be misleading. The subsequent refresh
// success/failure status conveys what happened in that case.
if !now.Before(existing.ExpiresAt) {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenExpired})
}
Comment thread config.go
Comment on lines +195 to +199
// Resolved here (not loadConfig) so the offline `token get` path can decide
// whether a refresh is due without building the full network config.
cfg.RefreshThreshold = getDurationConfig(
flagRefreshThreshold, "REFRESH_THRESHOLD", defaultRefreshThreshold)

- Resolve REFRESH_THRESHOLD with a dedicated uncapped parser so legitimate values above 10m (e.g. 30m, 1h) are honored; a large threshold only refreshes sooner and can't hang the CLI
- Document that the threshold, unlike timeouts, is not capped

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

- Add an EventTokenRefreshing status so proactive (near-expiry) refreshes surface a "Refresh token" step with accurate "nearing expiry" text, instead of either no feedback or a misleading "expired" label
- Reuse one neutral "Refresh token" step for expired and proactive refreshes, resolved by success/failure
- Drop the "starting new authentication flow" line from the refresh-failed status so it reads correctly whether the CLI degrades to the cached token or re-authenticates
- Emit the failure status on every failed refresh so the step never dangles in progress

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread main.go
Comment on lines +89 to +131
// Capture now once so the refresh decision and the graceful-degradation
// check below reason about the same instant.
now := time.Now()
if !needsRefresh(existing, cfg.RefreshThreshold, now) {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenStillValid})
storage = &existing
flow = "cached"
} else {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenExpired})
newStorage, err := refreshAccessToken(ctx, cfg, existing.RefreshToken)
if err != nil {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshFailed, Err: err})
// reuseValid reports whether the old token is still usable as-is —
// present and not yet expired. Mirrors ensureFreshToken so a corrupt
// token (empty access token but future expiry) is never reused.
reuseValid := existing.AccessToken != "" && now.Before(existing.ExpiresAt)
if existing.RefreshToken == "" {
// No refresh token means a refresh can only fail, so skip the
// network call entirely (no refresh step is shown). Reuse the old
// token while it's still valid; otherwise fall through to re-auth.
if reuseValid {
storage = &existing
flow = "cached"
}
} else {
storage = newStorage
flow = "refreshed"
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshSuccess})
// About to refresh: show an accurate in-progress status —
// "expired" only when actually expired, otherwise "near expiry".
if now.Before(existing.ExpiresAt) {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenRefreshing})
} else {
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventTokenExpired})
}
newStorage, refreshErr := refreshAccessToken(ctx, cfg, existing.RefreshToken)
if refreshErr != nil {
// The refresh genuinely failed, so mark it failed in the UI.
// Then degrade gracefully (reuse the still-valid token) or
// fall through to re-authentication once expired.
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshFailed, Err: refreshErr})
if reuseValid {
storage = &existing
flow = "cached"
}
} else {
storage = newStorage
flow = "refreshed"
ui.ShowStatus(tui.StatusUpdate{Event: tui.EventRefreshSuccess})
}
…FreshToken

- Extract tokenUsable so the "reuse the existing token" condition (non-empty access token and not yet expired, including the expiry boundary) is defined once and used by both the main flow and token get, removing the duplicated inline check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread auth.go
Comment on lines +137 to +155
// A network refresh is required, so upgrade to the full network-capable
// config now (deferring SERVER_URL validation and transport setup until
// this point keeps the far-from-expiry path offline).
full := cfg
if loadFull != nil {
full = loadFull()
}

// Resolve endpoints lazily too. Callers that pre-populate Endpoints skip
// this (e.g. tests).
if full.Endpoints.TokenURL == "" {
resolveEndpoints(ctx, full)
}

newTok, err := refreshAccessToken(ctx, full, tok.RefreshToken)
if err != nil {
return reuseOrFail(err)
}
return *newTok, true, nil
- Pin the lazily-built full config's store and client ID to the originally loaded ones in ensureFreshToken, so a refreshed token is saved to the same backend it came from rather than a freshly resolved store that could differ (e.g. TOKEN_STORE=auto), which would cause repeated refreshes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread tui/simple_manager.go Outdated
// flow, which prints its own status.
fmt.Printf("Refresh failed: %v\n", update.Err)
fmt.Println("Starting new authentication flow...")
case EventNewAuthFlow, EventNoExistingTokens:
Comment thread config.go
Comment on lines +195 to +197
// Resolved here (not loadConfig) so the offline `token get` path can decide
// whether a refresh is due without building the full network config.
cfg.RefreshThreshold = getRefreshThresholdConfig(flagRefreshThreshold)
Comment thread README.md Outdated
| `USERINFO_TIMEOUT` | `10s` | `/oauth/userinfo` request |
| `DISCOVERY_TIMEOUT` | `10s` | OIDC Discovery fetch |
| `REVOCATION_TIMEOUT` | `10s` | RFC 7009 revocation request issued by `token delete` |
| `REFRESH_THRESHOLD` | `5m` | Refresh the stored access token when it expires within this window; used by 'token get' and the main flow. No network request while the token is further from expiry |
…rding

- Give EventNewAuthFlow its own "Starting authentication flow..." message so it no longer claims "no existing tokens" when re-auth follows a failed refresh of existing tokens
- Update the loadStoreConfig comment to reflect that token get may load network config lazily and refresh when near expiry
- Use backticks around `token get` in the REFRESH_THRESHOLD reference row for consistency

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@appleboy appleboy merged commit 43f2556 into main May 24, 2026
20 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.

3 participants