LibreChat/packages/api
Danny Avila 928b14f5bc
🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server (#14596)
* 🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server

Concurrent refresh-token redemptions (tool-call 401, ping, reconnect
retries, expired-token reads) each replayed the same stored refresh
token at the OAuth token endpoint. RFC 9700 reuse detection treats the
replay as theft and revokes the entire grant family, forcing manual
re-consent every access-token expiry.

MCPTokenStorage.forceRefreshTokens is the choke point every refresh
path converges on; it now single-flights redemptions per
(tenantId, userId, serverName) so concurrent callers share one wire
call and receive the same rotated result. The refresh token is re-read
from storage inside the locked execution — never from a caller
snapshot — so a redemption starting after another refresh completed
uses the rotated token instead of replaying the consumed one.

Fixes #14583

* 🧪 test: Isolate Single-Flight Keys per Test via Unique Server Names

* 🔒 fix: Evict Stalled Refresh Slots, Decouple Waiter Aborts from Shared Redemption

Codex review round 1:
- A redemption that never settles no longer wedges the single-flight
  slot until process restart: a stale-entry timer evicts the map entry
  so later refreshes start fresh, while existing waiters keep their
  promise.
- Caller AbortSignals no longer thread into the shared redemption. An
  impatient waiter (silent refresh's short timeout) resolves its own
  wait with null via a per-waiter race; the shared wire call proceeds
  for everyone else, bounded by transport timeouts plus eviction.

* 🔒 fix: Abort Stalled Refreshes Before Slot Release, Hook Cache Invalidation to Redemption

Codex review round 2:
- The stale timer now aborts the wedged execution instead of deleting
  its slot; the slot frees only once the execution has settled, and an
  abort guard before the token-endpoint call stops a woken pre-wire
  stall from replaying a refresh token a successor already rotated.
- New onRefreshSuccess hook runs inside the shared redemption after
  rotated tokens persist, so the silent-refresh path's mcp_get_tokens
  cache invalidation fires even when the initiating waiter timed out
  before the redemption completed.

* 📝 docs: Record Post-Dispatch Abort Recovery Rationale on Stale-Refresh Valve
2026-08-02 06:39:44 -04:00
..
src 🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server (#14596) 2026-08-02 06:39:44 -04:00
types 🔬 ci: Add TypeScript Type Checks to Backend Workflow and Fix All Type Errors (#12451) 2026-03-28 21:06:39 -04:00
.gitignore
babel.config.cjs
jest.config.mjs 📦 chore: update sanitize-html to latest (#14573) 2026-08-01 09:35:57 -04:00
jest.setup.cjs 🌱 fix: Inject Code-Tool Files Into Graph Sessions on First Call (+ read_file Sandbox Fallback) (#12831) 2026-04-27 08:56:39 +09:00
package.json 📦 chore: update sanitize-html to latest (#14573) 2026-08-01 09:35:57 -04:00
tsconfig-paths-bootstrap.mjs
tsconfig.build.json
tsconfig.json 📦 chore: npm audit fixes and Mongoose 8.23 TypeScript follow-ups (#12996) 2026-05-07 09:47:40 -04:00
tsconfig.spec.json 📦 chore: Update TypeScript Config for TS v7 (#12794) 2026-04-23 12:51:03 -04:00
tsdown.config.mjs 🌀 ci: Deterministic Circular Dependency Checks (#14579) 2026-08-01 14:43:26 -04:00