Skip to content

improvement(credentials): keep valid client-credential tokens through refresh outages and pace token retries by failure kind - #9925

Merged
waleedlatif1 merged 9 commits into
stagingfrom
improvement/credential-token-resilience
Nov 12, 2026
Merged

waleedlatif1 merged 9 commits into
stagingfrom
improvement/credential-token-resilience

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Makes client-credential token requests more resilient, following the patterns used by MSAL, Entra's resilience guidance and botocore. The changes apply to the per-instance mint cache, the shared coordinated resolver (Shopify, Linear, Atlassian, Vanta) and Vanta itself.

  • Still-valid tokens keep working. Tokens are refreshed early, inside the 5-minute window before expiry. If that refresh fails with an outage or throttling, the cached or stored token is served until 30 s before it expires. That margin is checked again after the failed refresh, so a slow attempt cannot carry the token past it. A refusal of the credentials still fails at once.

  • Retry timing depends on the kind of failure (shared policy in lib/credentials/token-request-resilience.ts):

    • Refusal (bad or revoked credentials): replayed for 30 s, keyed by the secret's fingerprint.
    • Throttled (429, or 503 with Retry-After): the provider's Retry-After is honoured (seconds or HTTP-date), clamped to 1 s to 5 min. Without one, wait 1 min, doubling up to 5 min.
    • Outage (other 5xx, timeout, network error, malformed or unreadable response): 5 s, doubling to 30 s while it lasts. Before the backoff, one retry inside the mint after about 1 s, only if the failed attempt was quick.
    • Each kind keeps its own streak. A 429 after a run of outages starts its backoff at 1 min and does not inherit the outage count.
  • Retry-After is carried on errors from every client-credential minter. TokenServiceAccountValidationError gets an optional retryAfterMs, read by throttledRetryAfterMs (429/503 only). It is filled in by:

    • the shared helpers: throwForProviderResponse, exchangeClientCredentialsGrant, Shopify and Vanta
    • every bespoke minter: Box, Zoom, DocuSign, NetSuite, Help Scout, Ramp, Jama, Zendesk, Salesforce, Zoho Desk
    • the CrowdStrike, SailPoint and ZoomInfo client errors, through a new optional field

    A replayed failure carries an optional replayForMs, which the per-instance layer honours exactly.

  • Coordinated resolver:

    • The failure is remembered once for every worker in the existing row. Remembered failures and the still-valid fallback are served without taking the application lock.
    • A refusal whose code the row cannot store (the previous build reads it as provider_unavailable) gets an additive refusal: true flag, so a replay never serves the stale token.
    • A throttled failure gets an additive throttled: true flag, so the throttling streak continues across workers and stays separate from an outage streak.
    • Vanta asks for a token at most every 15 s while exchanges fail, because Vanta allows 5 tokens a minute and each new token revokes the previous one. That includes a 200 whose body cannot be read.
  • Single-flight is unchanged: coalesceLocally per instance, the advisory lock across workers.

  • The new fallback warning leaves out the credential id in selector privacy mode, like every other log in credential-service.ts.

Type of Change

  • Bug fix
  • Other: resilience improvement

Testing

  • Unit tests in credential-service-cancellation.test.ts, errors.test.ts and client-credentials-grant.test.ts:
    • fallback while the token is valid, error once it has run out (including after a slow failed refresh), and no fallback after a refusal
    • the single in-mint retry
    • each replay window, the Retry-After cap and the backoff floors
    • a 429 after two outages waits about a minute, not the outage streak's longer wait
  • A Retry-After test for each bespoke minter, in the minter test files (new helpscout.test.ts).
  • Real-Postgres integration tests in coordinated-client-token.integration.ts and vanta-token.integration.ts, run one file at a time:
    • fallback across workers
    • the retry while holding the lock, shared by waiting resolutions
    • serving without taking the lock
    • backoff growth and reset after recovery
    • a throttled answer after an outage backs off from 1 min, and throttling that continues backs off longer for every worker
    • sweep retention
    • a refusal the row cannot store stays a refusal on replay
    • Vanta's 15 s pacing, including an unreadable 200 body
  • Rolling-deploy fixtures pin both earlier failure decoders. A refusal memo is still exactly {code,status}. An outage memo adds only consecutiveFailures, a throttle memo adds consecutiveFailures and throttled, and an unstorable refusal adds only refusal; the old decoders ignore all three. The replay length rides on the existing expiresAt.
  • Each guard was reverted and its tests went red.
  • For reviewers: check the 5-minute Retry-After cap, and that the retry while holding the lock stays inside the 15 s lock_timeout (2 s budget + about 1.2 s pause + one 10 s attempt).
  • Follow-up, not in this PR: the Microsoft app (Entra client-credentials) token cache has its own LRU and still has no stale fallback or failure memo.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Nov 12, 2026 2:54pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge, with no new actionable issues found.

Summary

Adds shared retry rules for client-credential token requests and keeps still-valid tokens available during refresh outages.

  • Client-credential token requests wait according to failure type.
  • Per-instance caches keep usable tokens through temporary refresh failures.
  • Shared token rows keep workers aligned during provider failures.
  • Vanta waits between failed token exchanges to protect its token limit.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Resolve token] --> B{Token fresh?}
  B -->|Yes| C[Return token]
  B -->|No| D[Refresh or replay remembered failure]
  D --> E{Result}
  E -->|Success| F[Save and return new token]
  E -->|Credentials refused| G[Remove old token and return error]
  E -->|Outage or throttling| H{Old token still allowed and valid?}
  H -->|Yes| I[Return old token during backoff]
  H -->|No| J[Return error during backoff]
Loading

Reviews (5) · Last reviewed commit: "fix(credentials): skip the fallback toke..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 41 files

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 41 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 41 files

Confidence score: 3/5

  • In apps/sim/lib/internal/sailpoint/client.ts, an unreadable 4xx error body can be treated as invalid credentials and delete a still-valid cached token during a refresh outage. Keep body-read failures distinct so the outage fallback can run.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/internal/sailpoint/client.ts">

<violation number="1" location="apps/sim/lib/internal/sailpoint/client.ts:230">
P2: An unreadable SailPoint error body on a 4xx is misclassified as `invalid_credentials`, so refresh outages can delete a still-valid cached token instead of using outage fallback. Preserve the body-read failure separately and map it as an unavailable/invalid-response failure while retaining the response status and `Retry-After`. 

(Based on your team's feedback about provider response body failures.)</violation>
</file>

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

…ilures

A 429 or 503 token answer now records its Retry-After (delay-seconds or
HTTP-date, RFC 9110 section 10.2.3) as an optional retryAfterMs on
TokenServiceAccountValidationError, through throwForProviderResponse and the
shared client-credentials grant, whose provider classifier receives it.
…h outages

The per-instance client-credential mint cache now:
- serves a cached token that is inside its five-minute early-refresh window
  but not expired when the refresh fails with an outage or throttling, as
  MSAL, Azure.Core and botocore do; a refusal of the credentials still fails
- retries a quick transient mint failure once after about a second
- replays failures by kind instead of a flat 30 s: a refusal 30 s, a
  throttled answer for its Retry-After (1 s to 5 min), an outage 5 s growing
  to 30 s while it continues

The policy lives in lib/credentials/token-request-resilience.ts so the
coordinated resolver can share it.
…he coordinated resolver

The coordinated client-credentials resolver (Shopify, Linear, Atlassian,
Vanta) now applies the same policy as the per-instance mint cache, through
the shared row every worker reads:
- a quick outage is retried once under the application lock, within the 15 s
  its waiters allow
- a refusal is remembered 30 s, a throttled answer for its Retry-After (1 s to
  5 min, Vanta's now included), an outage 5 s growing to 30 s while it lasts
- while an outage or throttling is remembered, the stored token for the same
  secret is served until 30 s before it expires; a refusal fails at once

Rolling deploys stay safe: a refusal is stored exactly as before, and outages
add only a consecutiveFailures key that previous decoders ignore (both prior
decoders are pinned in the integration tests). The replay length rides on the
row's existing expiresAt. Expired memos are kept five more minutes so the next
failure can extend the backoff; an older build's sweep only resets it.
…emembered failures without the lock

- A 429 that names no Retry-After is replayed for at least a minute, doubling
  up to the five-minute cap, instead of the 5 s outage curve.
- The outage backoff never undercuts Entra's 5 s first-retry floor through jitter.
- Vanta asks for a token at most once per 15 s while exchanges fail (no
  in-request retry, replay floored), within its documented 5 tokens a minute,
  through a new optional minExchangeIntervalMs on the coordinated resolver.
- A remembered failure and the still-valid fallback are served from committed
  rows before the application lock is taken, so a refresh outage no longer
  serializes every resolution behind the lock.
- retryAfterMs now only carries a provider's Retry-After; a replayed memo
  reports its remainder as the additive replayForMs, which the per-instance
  layer honors exactly.
- One decode/verify path for stored tokens; the stored-failure permanence
  check uses isPermanentTokenFailureCode instead of building an error.
…llback gaps

- Carry the token endpoint's Retry-After on 429/503 failures from every
  bespoke client-credential minter (Box, Zoom, DocuSign, NetSuite, Help
  Scout, Ramp, Jama, Zendesk, Salesforce, Zoho Desk) and through the
  CrowdStrike, SailPoint and ZoomInfo client errors, so their throttled
  answers wait as long as the provider asks instead of the fixed backoff.
- Recheck the 30 s expiry margin when the per-instance cache falls back
  to a still-valid token after a slow failed refresh.
- Map an unreadable Vanta token body to provider_unavailable so it is
  remembered for every worker and paced at 15 s like any failed exchange.
- Remember a refusal whose code the shared failure row cannot store with
  an additive `refusal` flag, so a replay never serves the stale token.
- Rename the credentials helper to throttledRetryAfterMs so it no longer
  shares a name with providers/retry.ts.
…ls, and keep throttle delays through SailPoint and CrowdStrike

- A refusal of the credentials drops the per-instance cached token and expires the shared
  token minted with that secret, so a later outage cannot serve it again.
- A previous build's provider_unavailable memo with a non-transient 4xx status replays as a
  refusal instead of serving the stored token.
- The SailPoint saved-credential minter returns the first 429 with its Retry-After instead of
  retrying until the exchange timeout swallows it.
- A CrowdStrike error whose body cannot be read keeps its status and Retry-After.
- Vanta logs a malformed or oversized token response apart from an unreadable stream.
…rror bodies and replay only coordinated failures locally

- A SailPoint token error whose body cannot be read keeps its status and Retry-After,
  as CrowdStrike's does; caller cancellation still propagates.
- A shared-token (Vanta) mint failure is replayed per instance only when the coordinated
  layer remembers it (replayForMs); a failure it does not remember asks again at once.
@waleedlatif1
waleedlatif1 force-pushed the improvement/credential-token-resilience branch from c6c8771 to 2a94a62 Compare November 12, 2026 14:11
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 41 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 41 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 4911dc1 into staging Nov 12, 2026
80 checks passed
@waleedlatif1
waleedlatif1 deleted the improvement/credential-token-resilience branch November 12, 2026 17:02

This branch was previously deployed

1 inactive deployment
Preview — 9067a854 Deployed Nov 12, 2026 by vercel[bot]
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.

1 participant