@@ -75,18 +75,6 @@ func newAuthSessionsClient(coreURL, token string) *api.Client {
7575 return api .NewClientWithBaseURL (token , coreURL ).WithAuthSessionsPath (coreAuthSessionsPath )
7676}
7777
78- // resolveAuthHostToken returns a bearer scoped for the auth host (entire-core).
79- // For the auth host's own origin the tokenmanager hits the same-host shortcut
80- // and returns the stored login JWT unchanged — keeping the entire:session
81- // scope that core's session endpoints (and /me) require, with no STS exchange.
82- func resolveAuthHostToken (ctx context.Context ) (string , error ) {
83- token , err := auth .TokenForResource (ctx , api .OriginOnly (api .AuthBaseURL ()))
84- if err != nil {
85- return "" , fmt .Errorf ("resolve auth-host token: %w" , err )
86- }
87- return token , nil
88- }
89-
9078// isKeychainTokenRejected reports whether err indicates the stored
9179// keyring token can't authenticate against entire-core. Failure modes that
9280// collapse into the single "the user must re-login" branch:
@@ -167,7 +155,7 @@ func newAuthStatusCmd() *cobra.Command {
167155 if err := requireSecureBaseURL (insecureHTTPAuth ); err != nil {
168156 return err
169157 }
170- target , err := resolveStatusTarget (auth .NewContextStore (), auth .Contexts , api .AuthBaseURL ())
158+ target , err := resolveStatusTarget (cmd . Context (), auth .NewContextStore (), auth .Contexts , auth . RefreshedLoginToken , api .AuthBaseURL ())
171159 if err != nil {
172160 return err
173161 }
@@ -208,6 +196,11 @@ type authSessionLister func(ctx context.Context, coreURL, token string) ([]api.A
208196// name. Injected for testability; production wires auth.Contexts.
209197type contextsProvider func () ([]* contexts.Context , string , error )
210198
199+ // loginTokenResolver returns a usable login JWT for a context, transparently
200+ // re-minting an expired one from the stored refresh token. Injected so status
201+ // tests don't reach the network; production wires auth.RefreshedLoginToken.
202+ type loginTokenResolver func (ctx context.Context , c * contexts.Context ) (string , error )
203+
211204// statusTarget is the resolved core to act against: the active context's
212205// CoreURL + its session token, or (no active context) the configured
213206// AuthBaseURL + legacy keyring entry. Shared by `auth status` (profile +
@@ -219,17 +212,29 @@ type statusTarget struct {
219212 totalContexts int
220213}
221214
222- // resolveStatusTarget picks the core + token for `entire auth status`. The
223- // active contexts.json context wins (so `auth use` retargets status onto that
224- // login server); otherwise it falls back to the legacy keyring entry keyed by
225- // the configured auth host.
215+ // resolveStatusTarget picks the core + token for `entire auth status` (and
216+ // `logout`). The active contexts.json context wins (so `auth use` retargets
217+ // status onto that login server); otherwise it falls back to the legacy keyring
218+ // entry keyed by the configured auth host.
219+ //
220+ // For the active context the token is resolved through resolveLogin, which
221+ // transparently re-mints an expired login JWT from the stored refresh token.
222+ // This is the point of the refresh: an expired-but-refreshable session must
223+ // report "logged in", not "re-login" — the same false negative
224+ // auth.ResolveControlPlaneTarget already avoids for org/repo/project/grant.
225+ // `logout` benefits too: the refreshed bearer can authenticate the revoke call
226+ // instead of failing on an expired token. When refresh fails (revoked family,
227+ // network, opaque token), we fall back to the stored token and let the /me
228+ // liveness probe be the arbiter — preserving the accurate "no longer valid"
229+ // outcome for a genuinely dead session (ErrReauthRequired → expired token →
230+ // 401 → re-login).
226231//
227232// A genuine contexts.json read/parse error is surfaced, not swallowed — a
228233// missing file reads as "no contexts" (no error), so an error here means the
229234// file is corrupt or unreadable, which the user must see. This keeps status
230235// symmetric with the control-plane commands (auth.ResolveControlPlaneTarget),
231236// which fail the same way rather than silently degrading to a stale identity.
232- func resolveStatusTarget (store tokenStore , listContexts contextsProvider , fallbackBaseURL string ) (statusTarget , error ) {
237+ func resolveStatusTarget (ctx context. Context , store tokenStore , listContexts contextsProvider , resolveLogin loginTokenResolver , fallbackBaseURL string ) (statusTarget , error ) {
233238 all , current , err := listContexts ()
234239 if err != nil {
235240 return statusTarget {}, fmt .Errorf ("load contexts: %w" , err )
@@ -239,6 +244,12 @@ func resolveStatusTarget(store tokenStore, listContexts contextsProvider, fallba
239244 if c .Name != current || c .CoreURL == "" {
240245 continue
241246 }
247+ // Prefer a refreshed token; fall back to the raw stored token so a
248+ // refresh failure degrades to today's behaviour rather than dropping
249+ // to the legacy entry.
250+ if tok , terr := resolveLogin (ctx , c ); terr == nil && tok != "" {
251+ return statusTarget {coreURL : c .CoreURL , token : tok , activeContext : c .Name , totalContexts : total }, nil
252+ }
242253 if tok , terr := auth .LoginTokenForContext (c ); terr == nil && tok != "" {
243254 return statusTarget {coreURL : c .CoreURL , token : tok , activeContext : c .Name , totalContexts : total }, nil
244255 }
0 commit comments