Commit graph

4636 commits

Author SHA1 Message Date
J.C. Bartle
8b5cea29bc 🛠️ fix: Bind OpenID session tokens to authenticated identity
Stamp OpenID session token state with the LibreChat user id, OpenID subject,
tenant id, and normalized issuer when tokens are stored.

Fail closed before OBO inline token reuse/refresh when the session token
identity does not match the current authenticated identity, preventing a stale
or mixed Express session from supplying another user's upstream assertion.

Also validate the normal /api/auth/refresh session-token reuse shortcut against
the signed marker-cookie user before returning cached session tokens.

Note: sessions created before this change carry no identity stamp and are
treated as a mismatch. This is self-healing — the reuse path forces a full IdP
refresh (which re-stamps the session) and the OBO path throws, surfacing as a
one-time re-authentication for active OBO users at deploy time. The session
re-stamps within one session lifetime (SESSION_EXPIRY, default 15 min).
2026-06-28 17:53:46 -04:00
J.C. Bartle
cc74590864 🛠️ fix: type-safety polish on OBO data layer
Replace refresh token bridge query/update Record<string, unknown> usage
with typed Mongoose FilterQuery and UpdateQuery definitions.

Harden OpenID marker cookie JWT expiry handling by converting refresh
expiry milliseconds to integer seconds and rejecting invalid or
non-positive durations.

Add focused CSRF tests for fractional refresh expiry values and invalid
expiry configuration.
2026-06-28 16:31:29 -04:00
J.C. Bartle
5a32d9eb3d 🛠️ fix: Guard OpenID bridge retry user identity
Extract the shared OpenID refresh/user-resolution flow in AuthController
so the normal refresh path and bridge-recovery retry use the same grant,
claims, issuer, user lookup, and diagnostic logging code.

Preserve the existing path-specific behavior: the normal path still owns
migration updates and 401 login redirects, while the bridge retry still
falls through to the existing 403 invalid-token response.

Add a bridge-recovery guard that rejects retry results whose resolved
user id differs from the signed openid_user_id cookie before issuing
tokens or re-storing the grace bridge. Cover both the successful
matching-user recovery and the mismatched-user rejection.
2026-06-28 16:03:42 -04:00
J.C. Bartle
1786da6c00 🛠️ fix: fail closed on OBO MCP user identity mismatch
Add an OBO-specific guard before MCP tool execution that requires the
effective invocation user and captured request user to both have ids and
to match. This prevents OBO tool calls from falling back to a separate
configurable.user_id identity after request-bound OBO context has already
been captured.

Keep the existing user id fallback behavior for non-OBO MCP calls.

Tests cover mismatched OBO users, missing user ids, and the matching-user
path ignoring a conflicting configurable.user_id.
2026-06-28 15:42:06 -04:00
J.C. Bartle
d3a6f78ff8 🛠️ fix: keep OIDC refresh bridge during recovery grace
After successful bridged refresh recovery, re-store the stale-cookie bridge
with a short grace TTL instead of deleting it immediately. This lets parallel
/api/auth/refresh requests that already sent the stale browser cookie recover
before they can observe the first response's Set-Cookie.

Retarget the bridge to the refresh token returned by the bridged retry so
B-to-C refresh-token rotation remains recoverable. The grace TTL is parsed with
math() and defaults to 60s, which shrinks the replay window from the original
REFRESH_TOKEN_EXPIRY bridge lifetime to the short recovery grace period.

Remove the now-unused explicit bridge delete path from the service and
data-schemas method surface. Add regression coverage for grace re-store,
identity symmetry, retry failure behavior, and same-key upsert replacement.
2026-06-28 12:15:43 -04:00
J.C. Bartle
27c6eb1e35 🛠️ fix: preserve OIDC refresh-token sync on save failures
Sync OpenID refresh-token cookie/bridge state before persisting the
session so a transient session-store failure cannot lose an IdP-rotated
refresh token.

Also trigger sync when the session refresh token differs from the
browser refresh-token marker, not only when the current grant rotates
the token. This lets later writable refreshes repair stale browser
cookies left behind by SSE refreshes.

Route refresh bridge identity through the shared identity helper with
the threaded OBO identity context, falling back to request/user context
when needed.

Add regression coverage for session-save failures, stale browser cookie
repair, non-writable bridge storage, and shared-helper identity fallback.
2026-06-28 11:31:39 -04:00
J.C. Bartle
9131186bd1 🛠️ fix: centralize OBO identity scoping
Add shared auth identity helpers for app user ids, OpenID subjects,
tenant ids, and normalized OpenID issuers.

Thread a non-placeholder-visible OBO identity context from the real
request user through MCP connection, tool-call, reinit, and refresh
paths. Keep tenantId and openidIssuer out of createSafeUser so MCP
user placeholders do not expose those fields.

Scope OBO token cache and in-flight exchange keys by tenant, issuer,
OpenID subject, scopes, and a SHA-256 hash of the upstream assertion.
This prevents cross-tenant/cross-issuer collisions and avoids reusing
tokens minted from stale rotated assertions.

Use the shared identity helpers for OpenID refresh-flight keys and
refresh-token bridge recovery records so related OBO refresh paths share
the same identity normalization rules.

The helper is intended for auth-boundary and credential-cache code, not
as a blanket replacement for ordinary app user id ownership checks.
2026-06-28 10:39:04 -04:00
J.C. Bartle
1018b81de5 Fix OpenID refresh flight retry and marker hydration
Allow failed OpenID refresh flights to be reclaimed immediately instead of pinning transient errors.

Preserve the browser refresh-token marker when joined refreshes hydrate session tokens from a shared flight result.

Stabilize AuthService tests by isolating mocked module imports from prior suites.
2026-06-26 10:59:52 -04:00
jcbartle
6398628e10
Merge branch 'main' into fix/feature/obo 2026-06-24 16:04:29 -04:00
J.C. Bartle
a2e8e838c0 Merge branch 'fix/feature/obo' of https://github.com/jcbartle/LibreChat into fix/feature/obo 2026-06-24 16:02:41 -04:00
J.C. Bartle
5fa35c7660 📦 test: Exclude RefreshTokenBridge from tenant-isolation coverage
Add RefreshTokenBridge to the tenant-isolation coverage allowlist because
refresh bridge lookups run during unauthenticated OpenID refresh recovery.
The controller first recovers user context from the signed OpenID marker
cookie, then the bridge methods apply explicit user and tenant filters.

Ambient tenant isolation would bind this recovery path to request-local
tenant context that is not available at the point the stale cookie is being
resolved
2026-06-24 15:46:23 -04:00
J.C. Bartle
9bb146cfe7 🙌 fix: keep OIDC bridge recovery success on cleanup failure
Make refresh-token bridge cleanup best-effort after a bridged OIDC
refresh succeeds. A transient delete failure now logs a warning but does
not convert the already-refreshed session and cookies into a 403 response.
2026-06-24 15:22:57 -04:00
J.C. Bartle
780122594a 🫙 fix: preserve stale OIDC cookie bridge key
Track the refresh token last written to the browser cookie separately
from the current session refresh token. When inline OIDC refreshes rotate
tokens without a writable response, keep bridging from the browser-stale
token directly to the latest session token.
2026-06-24 15:15:39 -04:00
J.C. Bartle
2b81c47d17 🌉 fix: store OIDC refresh bridge without cookie response
Treat missing or non-cookie responses like headers-sent streaming
responses during inline OIDC refresh. When the IdP rotates the refresh
token and cookies cannot be written, persist a recovery bridge so a later
/auth/refresh can recover after session expiry.
2026-06-24 14:55:07 -04:00
Danny Avila
9e74cc0e57
v0.8.7 (#13907)
Some checks failed
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
2026-06-24 14:49:32 -04:00
J.C. Bartle
7a52b89516 🔑 fix: include refresh token in OIDC local refresh flight key
Key the process-local OIDC refresh coalescing by the current session
refresh token, matching the Mongo-backed flight key. This prevents a
request with a newly rotated token from joining an older pending refresh
and inheriting its failure/result.
2026-06-24 14:43:27 -04:00
Danny Avila
c5610871d0
🐌 fix: Prevent ReDoS in YouTube URL Extraction for URL Context (#13937)
* 🛡️ fix: Prevent ReDoS in YouTube URL extraction for URL Context

The YouTube detection/strip regexes ran as a single global pass over
authenticated, user-controlled chat text. The engine could restart at every
`youtube.com/watch?` occurrence and the lazy `\S*?&` rescanned the rest of a
long non-whitespace token each time, giving quadratic CPU behavior that blocks
the Node event loop (DoS) for Google/Vertex agents with url_context enabled.

- Tokenize on whitespace and skip tokens longer than a real URL, and cap the
  total text scanned, so work is bounded to O(n). URLs never contain whitespace,
  so per-token matching is equivalent.
- Replace the lazy unbounded `(?:\S*?&)?` with the delimiter-bounded
  `(?:[^\s&]*&)*` (no behavior change for real URLs).
- Apply the same discipline to the strip path.
- Add ReDoS regression tests; a 3MB crafted input now completes in <10ms.

* 🛡️ fix: Bound the YouTube strip scan by the same total budget

Address Codex P1: the strip path applied only the per-token cap, so a valid URL
followed by many sub-cap malformed tokens still regex-scanned the entire message
(~1s on 3MB). Injected ids only come from the first MAX_YOUTUBE_SCAN_CHARS
(extraction's cap), so a link beyond that is never in injectedIds anyway; cap the
strip scan at the same budget and leave the tail verbatim. 3MB PoC: ~1s -> ~14ms.

* 🧬 fix: Make YouTube URL matching linear instead of capping the scan

The previous fix bounded the scan with per-token + total-scan caps, but the
total-scan cap discarded content: a URL near the end of a long prompt was missed
(extraction sliced to 100k), and large prepended file/quote context exhausted the
strip budget before the real URL (strip skipped it). Codex round 2 (P2 x2).

Replace the backtracking-prone matcher with a linear one: a single regex captures
host + path/query (greedy `[^\s]*`, bounded `{1,63}`/`{0,10}` subdomain repetition,
no lazy/ambiguous quantifier), and the video id is parsed from the capture
afterwards. This is O(n) over arbitrary input, so the scan caps (and the content
they discarded) are removed entirely. Extraction and stripping now scan the whole
message linearly.

Benchmarks (no caps): 3MB attack token ~3ms, 3MB many-token ~4ms, valid URL at end
of 3MB found in ~18ms. Adds regression tests for long-prompt extraction and
stripping past large prepended context.

* 🔡 fix: Match adjacent + capitalized YouTube URLs after linear rewrite

Codex round 3 (regressions from the linear matcher):
- Stop the path capture at URL-list delimiters (`,` `)` `]` `<` `>`, none of which
  occur in a real YouTube URL) so adjacent links in one token (comma-separated or
  markdown `](url1)](url2)`) are matched separately instead of swallowed.
- Lowercase the path segment before matching route names, since the detection regex
  is case-insensitive (`/WATCH?v=`, `/EMBED/`).

* 🔒 fix: Allowlist URL chars + bounded path parsing for YouTube matching

Codex round 4:
- Replace the path stop-char blocklist with an allowlist of characters that occur
  in real YouTube URLs, so adjacent links separated by any prose delimiter
  (`;`, `|`, etc.) are matched separately instead of swallowed.
- Parse the route with anchored, bounded regexes instead of `path.split('/')`, so a
  malformed path of millions of slashes no longer allocates a huge array / blocks
  the event loop. Also bounds the `v=` param read.

* 🎯 fix: Restrict YouTube matcher to recognized video routes

Codex round 5: a nested video URL inside an unrecognized YouTube URL
(`youtube.com/redirect?q=https://youtu.be/<id>`) was swallowed by the greedy
match and missed. Restrict the matcher to recognized single-video forms
(youtu.be/<id>, /(shorts|live|embed|v)/<id>, /watch?<query>) so an unrecognized
route doesn't match and the global scan continues into the nested link. Stays
linear (verified: 3MB redirect/slash/host floods all <25ms) and keeps the
allowlist tail so adjacent links still split. Adds nested-URL + unrecognized-route
regression tests.

* 🎬 fix: Find nested watch links + skip malformed v= duplicates

Codex round 6 (P3 watch-query edges):
- Drop `:` from the path allowlist. It never occurs in a real YouTube path/query,
  but `://` of a nested URL does — so `watch?url=https://youtu.be/<id>` now stops
  the watch match and the scan finds the nested link.
- Scan every `v=` param and return the first valid 11-char id, so a malformed
  earlier `v=` (e.g. `watch?v=tooShort&v=<valid>`) no longer shadows a later valid one.

* 🧹 fix: Strip whole YouTube URL incl. colon-containing trailing params

Codex round 7: dropping `:` from the tail (round 6) made the strip path stop mid-URL
on a URL-valued param (`watch?v=<id>&next=https://example.com`), leaving `://example.com`
orphaned. Use a separate strip matcher whose tail re-includes `:` so the whole URL token
is removed, while detection keeps the `:`-excluded tail to still find nested video links.
Also corrects a stale "per-token cap" comment left over from the linear rewrite.
2026-06-24 13:06:59 -04:00
jcbartle
48101f5519
Merge branch 'main' into fix/feature/obo 2026-06-24 12:10:03 -04:00
Danny Avila
189cb245c2
🫥 fix: Hide Quote Popup When Selection Collapses Silently (#13936)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
The "Add to chat" popup lingered over an empty caret after a selection collapsed through a path that fires no mouse/key event — most often a streaming markdown re-render replacing the selected text node. The selection state only updated on mouseup/dblclick/keyup/scroll/resize, so a silent collapse left the button stranded ("showing up with nothing selected").

Add a `selectionchange` listener that hides the popup the instant the selection collapses or empties. It only hides, never shows, so an in-progress drag-select still won't flicker the popup.

Adds an e2e that collapses the selection without a mouse event and asserts the popup disappears.
2026-06-24 11:24:42 -04:00
Danny Avila
82662443e5
🧱 ci: Retry Failed Docker Builds (#13935)
* ci: retry failed Docker build jobs

* ci: skip stale Docker build retries

* ci: handle Docker retry edge cases
2026-06-24 10:09:36 -04:00
Danny Avila
ef1ee6ee16
🪤 fix: Guard Prompts Popover Against Empty Result Keyboard Navigation (#13931)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🛡️ fix: Guard Prompts and Mention popovers against empty-result navigation

* 🛡️ fix: Prevent Tab default and clear stale filter on empty popover close
2026-06-23 23:14:24 -04:00
Danny Avila
1662adc581
📺 feat: Google URL Context Param with Native YouTube Video Understanding (#13924)
*  feat: Add Google url_context Param with Native YouTube Video Understanding

Mirror the web_search grounding wiring for a new Google/Gemini `url_context`
model param (resolves to the native `urlContext` tool). When enabled, YouTube
URLs in the latest user message are injected as Gemini video parts (fileData),
since the URL Context tool does not support YouTube.

* 🎞️ fix: Provider-aware YouTube injection limits for url_context

Address Codex review on the YouTube video-understanding path:
- Cap injected YouTube parts per request by provider/model (Vertex: 1; Gemini
  Developer API: 10 on 2.5+, 1 on earlier models) so multi-link messages cannot
  exceed the provider limit and get rejected.
- Set a video/mp4 mimeType on Vertex YouTube fileData (matching Vertex samples);
  the Developer API still omits it.

* 🧩 fix: Round-trip url_context for Google-compatible custom endpoints

Add url_context to openAIBaseSchema so the per-chat value persists for custom
endpoints configured with customParams.defaultParamsEndpoint: 'google', matching
how web_search is already picked there.

* 🚦 fix: Gate url_context tool to Gemini 2.5+ models

Per Google's URL Context supported-models list (2.5+/3.x only), skip the native
urlContext tool on earlier models (debug-log + no-op) instead of sending it and
triggering a provider 400. This also gates the coupled YouTube video-understanding
injection to 2.5+, since it keys off the resolved urlContext tool.

* ✂️ fix: Strip YouTube URLs from urlContext text; keep url_context out of OpenAI schema

- Remove url_context from the shared openAIBaseSchema (revert): it is Google-only
  and would otherwise leak as an unsupported param to OpenAI/Azure/OpenRouter
  requests. On Google-compatible custom endpoints url_context is enabled via admin
  addParams/defaultParams, same as web_search.
- When injecting YouTube video parts, strip the matched YouTube URLs from the prompt
  text so the urlContext tool (which reads URLs from text and cannot fetch YouTube)
  does not consume its URL budget on them. Non-YouTube URLs are left intact.

* 🎯 fix: Refine url_context model gating and YouTube injection edges

Address Codex round 4:
- Exclude non-text modality variants (image/live/tts) from URL Context support,
  mirroring the Google tool-combination modality exclusion.
- Use the resolved run model (model_parameters.model) for YouTube injection limits
  instead of the saved base model.
- Strip only the YouTube links actually routed to video (id-aware); keep over-limit
  links in the text so the model can still reason about them.
- Keep timestamped YouTube links (?t=/&start=) in the text so the moment cue survives.
- Recognize youtube-nocookie.com/embed links.

* 🎚️ fix: Exclude audio Gemini variants + preserve pre-id YouTube timestamps

Address Codex round 5:
- Add `audio` to the url_context modality exclusion so audio-only Gemini variants
  (e.g. gemini-2.5-flash-preview-native-audio-dialog) skip the tool instead of 400ing.
- Detect YouTube timestamps anywhere in the matched URL (incl. before `v=`, e.g.
  watch?t=90&v=<id>), so timestamped links are kept in the prompt text as intended.
2026-06-23 22:42:06 -04:00
Oliver777int
6934d07066
🌵 fix: Align Mention Empty Result Behavior With Skills Command (#13928)
Co-authored-by: oliver.olsson <oliver.olsson@zeekrtech.eu>
2026-06-23 21:10:06 -04:00
Danny Avila
d9a76fca90
🧠 feat: Configurable Reasoning Replay for Custom Endpoints (#13921)
* 🧠 feat: Configurable Reasoning Replay for Custom Endpoints

Adds customParams.includeReasoningContent so OpenAI-compatible custom endpoints (e.g. Xiaomi MiMo, Kimi) can replay reasoning_content on tool-call turns natively, without impersonating the moonshot provider.

* 🔁 feat: Replay reasoning_content across turns for opted-in custom endpoints

Extends the DeepSeek reasoning-content format spoof to honor customParams.includeReasoningContent, so custom OpenAI-compatible endpoints (Xiaomi MiMo, Kimi) reconstruct reasoning_content from persisted history on later turns, matching DeepSeek thinking-mode parity. Adds shouldReplayReasoningContent predicate (tested) and surfaces the flag on the initialized agent.

* 🪢 refactor: Split within-run vs cross-turn reasoning replay flags

moonshot only replays reasoning_content within a run's tool calls, not across turns. Decouples the two: includeReasoningContent = within-run replay (exact moonshot parity), new includeReasoningHistory = cross-turn reconstruction from persisted history (implies includeReasoningContent, since reconstruction is a no-op without the within-run replay flag).

* 🩹 fix: Apply reasoning replay across all param-format branches

Move the within-run includeReasoningContent application out of the OpenAI-only branch in getOpenAIConfig to after the branch dispatch, so custom endpoints using anthropic/google defaultParamsEndpoint gateway modes also honor includeReasoningContent/includeReasoningHistory. Addresses Codex finding.

* chore: Update @librechat/agents to v3.2.46

* 🧽 refactor: De-spoof reasoning replay via explicit preserveReasoningContent

Now that @librechat/agents 3.2.46 exposes an explicit preserveReasoningContent option on formatAgentMessages, pass it directly instead of impersonating provider: deepseek. Behavior is unchanged (shouldReplayReasoningContent still gates DeepSeek + the custom includeReasoningHistory flag); also corrects the comment to reference includeReasoningHistory.

* 🌳 fix: Walk subagents in the reasoning-history replay gate

The gate only checked the primary agent and top-level handoff/parallel configs, so an opted-in custom endpoint used solely as a nested subagent had its persisted reasoning dropped on later turns. New exported anyAgentReplaysReasoningContent walks subagentAgentConfigs (cycle-safe, mirrors anyAgentHasCodeEnv); client.js uses it. Addresses Codex finding.
2026-06-23 21:08:47 -04:00
jcbartle
9356e4d660
Merge branch 'main' into fix/feature/obo 2026-06-23 18:50:28 -04:00
J.C. Bartle
033bdf4c25 Keep OpenID marker cookies aligned on inline refresh
Refresh token_provider and openid_user_id with the same expiry as the
rotated refreshToken cookie when an inline OBO refresh can still write
headers.

Share the marker-cookie writer with the normal OpenID auth refresh path
so the fallback /api/auth/refresh branch continues to recognize valid
OpenID refresh tokens after session expiry.
2026-06-23 18:42:25 -04:00
J.C. Bartle
6a2a17b786 🤝 Coordinate OIDC inline refreshes across workers
Add a short-lived Mongo-backed refresh-flight record so concurrent
OBO refreshes for the same OpenID session do not redeem the same
rotating refresh token on different workers.

The winning worker performs the IdP refresh and stores an encrypted
result; joiners wait for that result, hydrate their request session,
and return without calling the IdP.
2026-06-23 17:52:41 -04:00
J.C. Bartle
164ebaf09d 🌉 Persist OIDC refresh-token recovery bridges in MongoDB
Store SSE OBO refresh-token recovery bridges in MongoDB instead of
process-local memory so /api/auth/refresh can recover after worker
restarts or cross-worker routing.

Derive bridge expiry from REFRESH_TOKEN_EXPIRY so the recovery window
matches the stale refreshToken cookie it repairs, and delete bridges
after successful recovery.
2026-06-23 17:19:57 -04:00
Dan Lew
f09a1ad7fc
🧹 fix: Order Enabled Guard After Config Expansion in useGetAgentByIdQuery (#13927)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Otherwise, it's possible for a config to override the `isValidAgentId` check.

Without that check, it's possible to query `getAgentById()` with a blank `agent_id`,
which can result in polluting the `QueryKeys.agent` cache with a full list of agents
(instead of just a single agent result).
2026-06-23 17:15:44 -04:00
Danny Avila
0a3448dcee
🧭 fix: Harden User Provided Endpoint URL Protection (#13919)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-06-23 16:35:16 -04:00
Danny Avila
562bd8ec5f
🐛 fix: Prevent Infinite Render Loop on Code-Execution File Preview (#13922)
* 🐛 fix: Prevent Infinite Render Loop on Code-Execution File Preview

Loading a conversation that contains a large (>1MB) code-execution
office file crashed the whole app with React error #185 ("Maximum
update depth exceeded") on hard refresh.

Root cause (client-only): the terminal-write effect in
useAttachmentPreviewSync writes the resolved preview record back into
messageAttachmentsMap with a fresh object identity on every run, and
`attachment` is in the effect's dependency array. useAttachments
re-derives `attachment` ({...db, ...liveEntry}) with a new identity on
every map write, so once polling resolves (pending -> ready on a loaded
conversation) the effect ping-pongs forever:
setAttachmentsMap -> re-derive -> effect -> setAttachmentsMap.

Only files large/slow enough to defer extraction are persisted at
status: 'pending', which is why small documents never triggered it.

Fix: an idempotency gate that bails before setAttachmentsMap when the
merged attachment already carries the resolved status/text/textFormat/
previewError. The write happens once and then settles.

Tests:
- useAttachmentPreviewSync.loop.spec.tsx wires the real
  useAttachments -> hook feedback to reproduce the loop (verified to
  throw #185 without the gate, settle with it).
- e2e/specs/mock/attachment-preview-loop.spec.ts loads a conversation
  with a pending code-exec attachment whose preview resolves ready and
  asserts the app does not crash.

Closes #13916

* 🔧 feat: Make Office Preview Extraction Cap Configurable (default 2MB)

The inline code-execution preview extraction ceiling was a hardcoded 1MB
constant (MAX_TEXT_EXTRACT_BYTES). Office/text artifacts over that skip
the inline preview and resolve to "Preview unavailable" (download-only).

Make it configurable via FILE_PREVIEW_MAX_EXTRACT_BYTES and raise the
default to 2MB so larger documents get an inline preview out of the box.
The rendered HTML remains independently capped at MAX_TEXT_CACHE_BYTES
(512KB), so image-heavy files over that still fall back to the existing
"preview too large" banner rather than rendering unbounded output.

- resolveMaxTextExtractBytes(env) parses the override, falling back to
  2MB on missing/non-numeric/non-positive values (warns on invalid).
- Documented in .env.example next to the other file-size limits.
- Unit tests cover default, valid override, fractional flooring, and
  invalid fallback.

* 🐛 fix: Guard sub-byte preview cap from flooring to zero

A fractional FILE_PREVIEW_MAX_EXTRACT_BYTES in (0, 1) passed the
positive-number check then floored to 0, making MAX_TEXT_EXTRACT_BYTES
zero and treating every non-empty artifact as oversized. Floor first,
then require the result to be >= 1 byte before accepting it; otherwise
fall back to the 2 MB default. Adds coverage for the sub-byte case.

*  test: Make exported-ceiling assertion env-independent

The "exported ceiling" assertion compared MAX_TEXT_EXTRACT_BYTES to a
literal 2 MB, but that const is initialized from
FILE_PREVIEW_MAX_EXTRACT_BYTES at module load — so the suite would
falsely fail when run with the override set. Assert the export tracks
resolveMaxTextExtractBytes(env) for the current environment instead; the
undefined-case test continues to pin the 2 MB default.
2026-06-23 16:34:43 -04:00
Danny Avila
f616a58fb7
🖱️ fix: Summon Quote Popup on Double-Click Word Selection (#13923)
* 🖱️ fix: Summon Quote Popup on Double-Click Word Selection

Chromium commits a double-click word selection on the `dblclick` event, after `mouseup` has already read a still-collapsed range, so the "Add to chat" popup never appeared for double-click selections. Listen for `dblclick` in addition to `mouseup`/`keyup`.

Adds an e2e covering a native double-click word selection (measured-coordinate dblclick exercises the real browser path, unlike the programmatic-Range helper).

* 🎯 test: Target Reply Text Node in Double-Click Quote E2E

Walk to the text node containing the needle (not the first text node in .message-render, which may be a select-none screen-reader/model-label header) and measure the needle's first character, so the native double-click lands on the reply word rather than metadata.
2026-06-23 15:52:34 -04:00
Danny Avila
1eb460eb03
🧾 fix: Harden Historical File Authorization (#13918)
* fix: Harden historical file authorization

* chore: Sort file authorization imports

* fix: Preserve authorized historical artifact refs

* chore: Format historical artifact hardening
2026-06-23 15:49:57 -04:00
Danny Avila
606292c5c5
🤐 fix: Withhold Custom Endpoint Headers for User URLs (#13917)
* fix: withhold custom endpoint headers for user URLs

* fix: require user key for user custom URLs

* test: type custom endpoint header cases

* fix: prompt for keys on user custom URLs
2026-06-23 15:49:31 -04:00
Danny Avila
f14309e087
🪶 refactor: Ground Default Model Spec Selection in Conversation Recency (#13915)
Resolve the new-chat default spec from the most recent conversation setup
(LAST_CONVO_SETUP_0) instead of reconstructing intent from accumulated
cross-endpoint history. Removes hasStoredModelValue, hasStoredPrefixValue,
hasStoredModelSelection, the sticky LAST_SPEC read, the nested
resolveSoftDefault closure, and the duplicated prioritize/modelSelect branches.

Fixes the soft default being dropped on New Chat ("Select a model") when its
preset endpoint sits outside modelSpecs.addedEndpoints alongside a custom
endpoint: a model lingering in LAST_MODEL for that endpoint no longer
suppresses the soft default.

Clear All Chats now also clears LAST_SPEC/LAST_MODEL/LAST_TOOLS so a new chat
afterward cleanly returns to the soft default. Adds the cross-endpoint unit
case, a clearAllConversationStorage test, and a cold-load e2e regression test.
2026-06-23 15:49:04 -04:00
J.C. Bartle
13d118a2ea Merge branch 'fix/feature/obo' of https://github.com/jcbartle/LibreChat into fix/feature/obo 2026-06-23 14:10:48 -04:00
J.C. Bartle
2d78af5445 🔨 fix: hydrate joined OIDC refresh sessions with stable refresh tokens
Update single-flight OIDC refresh joiners whenever refreshed access token
state changes, even if the IdP keeps the refresh token unchanged.

This prevents joined requests from retaining stale accessToken or
accessTokenExpiresAt values and redundantly refreshing later in the same run.
2026-06-23 14:10:39 -04:00
J.C. Bartle
105f49100a 🔒 fix: Recover OIDC refresh-token rotation after SSE OBO refresh
When an inline OBO refresh rotates the OpenID refresh token after SSE headers
have already been sent, the browser refreshToken cookie cannot be updated. Store
a short-lived encrypted bridge from the stale cookie token to the rotated token
so /api/auth/refresh can recover after express-session loss.

Use the signed openid_user_id cookie to load user context for bridge validation,
retry only on invalid_grant, and delete the bridge only after the bridged refresh
succeeds.
2026-06-23 14:01:01 -04:00
jcbartle
b49e7b4ae3
Merge branch 'main' into fix/feature/obo 2026-06-23 12:41:09 -04:00
J.C. Bartle
bf251b820f ♻️ refactor: Pass OBO upstream-token closure into MCP instead of req
Build the OpenID upstream-token provider at the request boundary and thread
only the closure through MCP handling, so the MCP service layer no longer
receives the raw Express request. The closure still reads/refreshes the live
session at tool-call time, preserving the walk-away recovery.

- Drop `req`/`capturedReq` from createMCPTools, createMCPTool, reconnectServer,
  createToolInstance, and reinitMCPServer; forward `upstreamTokenProvider`
  instead. Closure is constructed in loadTools, loadToolDefinitionsWrapper, and
  the reinitialize route, where req/res are in scope.
- OBO: fall back to user.federatedTokens when the provider yields no live
  session, so OIDC remote-agent calls (verified bearer, no session) still work.
- Inline refresh: mirror a rotated refresh token to the refreshToken cookie via
  a shared setRefreshTokenCookie helper, guarded by !res.headersSent (no-op on
  the streaming path; session copy stays authoritative).
- Single-flight: hydrate a joining request's own session from the resolved
  tokens so a later OBO call doesn't replay a rotated-away refresh token.

Addresses owner feedback and three review findings.
2026-06-23 11:59:01 -04:00
Danny Avila
33d7b0070c
🌍 i18n: Update translation.json with latest translations (#13914) 2026-06-23 11:14:38 -04:00
Danny Avila
bc6b032421
🛑 refactor: Demote User Abort Logs (#13904)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Demote user abort logging

* fix: Handle abort causes

* fix: Demote user-aborted agent completion to debug log

The error users still saw originated in AgentClient's completion catch,
which logged every caught error (including user aborts) at error level
before checking the abort signal. Branch on abortController.signal.aborted
so user-initiated aborts log at debug while real failures stay error-classified.

Also give the handleAbortError it.each cases distinct titles.
2026-06-23 09:55:21 -04:00
Danny Avila
cb2bafb457
🧂 chore: Require an Operator-Supplied Admin Panel Session Secret (#13902)
* fix: require admin panel session secret

* 🩹 fix: Plain-Expand Admin SESSION_SECRET So Compose Maintenance Commands Run

The `${VAR:?}` required form fails interpolation for every deploy-compose
subcommand (down/pull/config), breaking `npm run update:deployed` for installs
whose .env predates ADMIN_PANEL_SESSION_SECRET. Plain expansion keeps those
commands working; the admin-panel image fail-fasts on an empty secret, so the
panel still refuses to start without it.
2026-06-23 08:43:54 -04:00
Danny Avila
77854decdf
🪣 fix: Cap Context Projection Workload Before Tokenization (#13910)
* fix: bound context projection workload

* fix: Address context projection CI failures

* fix: Bound context projection database reads

* fix: Sort projection spec imports

* fix: Cap projection body reads with stats
2026-06-23 08:43:09 -04:00
Danny Avila
2f800c5b52
💰 fix: Bound MCP tools/list Pagination with Aggregate Budgets (#13909)
* fix: bound MCP tools pagination budgets

* fix: enforce MCP tools pagination budgets

* fix: cancel MCP tools list timeouts

* test: type MCP tools timeout mock
2026-06-23 08:34:09 -04:00
Danny Avila
d9251fc3b6
✂️ fix: Cap Audit Chain Verification and Honor Client Cancellation (#13903) 2026-06-23 08:33:16 -04:00
Danny Avila
ddc763595a
🍪 fix: Validate Shared-File Cookie Auth Against the Live Refresh Session (#13908)
* fix: validate shared file cookie sessions

* fix: run shared file session lookup as system
2026-06-23 08:32:28 -04:00
Danny Avila
725a14e409
🪜 fix: Strip Caller-Supplied Priority from Assign-Only Config Upserts (#13911)
* fix: prevent assign-only config priority changes

* fix: preserve assign-only config priority atomically

* style: format config priority guard

* fix: type preserve priority upsert option
2026-06-23 08:31:26 -04:00
Danny Avila
edc0aebdb9
🛂 fix: Re-Check execute_code Authorization on Event-Driven Tool Loads (#13912) 2026-06-23 08:30:39 -04:00
Danny Avila
e807c63d5d
🔐 fix: Gate Shared Startup Config By Link Access (#13897)
* fix: gate shared startup config by link access

* fix: satisfy shared config CI checks

* fix: align shared config client types

* fix: reject expired shared link access
2026-06-23 08:28:37 -04:00