Closes StrykerJS survivors on the diff: the cap at exactly 365 days against a
second over, the rotating-exchange failure's typed code (and that a
non-rotating provider is unaffected), and the start refusal's error word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token answer without a usable expires_in (absent, non-numeric, a string that
is not a positive integer) now takes the descriptor's
defaultAccessTokenLifetimeSeconds (3600 for every provider), so no issued
token has a NULL expiry that dueForRefresh would never refresh. A 2xx whose
refresh_token is present but not a string is the typed
provider_body_not_token_set (502 managed_credential_refresh_failed, tokens
kept); an empty string stays a missing token, so a rotating provider still
fails closed. The expiry rule is now pinned on memory and both live Postgres
rigs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A grant verdict is read from a 400 body by exact code, never from a 401, a
5xx or a 2xx. A 2xx that is not an object with a non-empty string
access_token and no error is refused as provider_body_not_token_set, so a
malformed success keeps the tokens and surfaces as the typed 502 for every
provider, rotating or not. A refresh answer without expires_in stores the
expiry as unknown rather than keeping the already-due one; an unknown expiry
is never due, so the next resolve does not refresh again.
Pins Soul's three surviving mutants: substring code match, the dropped
expiry rule, and the snapshot refresh token on a non-rotating provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Added after a diff-scoped StrykerJS run left survivors: no-lock paths, the
skew boundary, decision kinds, a vanished row, open-transaction hygiene on
the live store, and the token request's method, signal, token type and
scope handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refreshLinkedIdentityCredentialIfNeeded is deleted. refreshManagedCredential
owns the policy; HeimdallStore.updateLinkedIdentityCredential owns the row
(SELECT ... FOR UPDATE in Postgres, a per-id chain in memory), so a refresh
token is presented once across concurrent requests and processes. invalid_grant
clears the tokens (410 managed_credential_revoked); any other failure keeps
them (502 managed_credential_refresh_failed). The "?? refreshToken" keep-old
fallback is gone; keeping it is an explicit branch for non-rotating providers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Endpoint, client-auth method (RFC 7591 names) and rotation now live on
ProviderDescriptor; requestProviderTokens is the only code that talks to a
token endpoint. Failure labels are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>