mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 19:41:32 +00:00
* ✨ feat: Custom Request Headers For Langfuse Self-hosted Langfuse behind an authenticating proxy or gateway could not be reached: every outbound Langfuse request hardcoded `Authorization` and nothing else. Adds `langfuse.headers`, mirroring `endpoints.custom` headers, and applies it to all four request surfaces — trace/media export (via the agents run config), feedback scores, central project-identity lookup, and admin credential verification. Values resolve through the same pipeline as endpoint headers, so `${ENV_VAR}` interpolation and header-safe encoding come along. `extractEnvVariable` continues to refuse infrastructure secrets, so a config cannot exfiltrate `MONGO_URI` through a header. A header whose variable is unset is dropped with a one-time warning rather than sent as a literal `${...}`, which a gateway would read as a wrong credential instead of a missing one. Headers merge beneath LibreChat's own `Authorization` on the REST surfaces, matching `mergeHeaders`, so a custom header can never displace the Langfuse credential. These are deployment-level and documented as such: trace export batches spans from every user through a single exporter, so unlike endpoint headers they cannot carry per-user placeholders. The central project-id cache key now includes the headers, so the header-less module warm-up cannot record a proxy rejection against the entry the request path later reads. * 🔒 fix: Keep Langfuse Headers Out Of Stored Overrides The generic admin config API accepts any field path inside an allowed section, so `langfuse.headers` could be written through it. Unlike `langfuse.secretKey`, headers are a map rather than one scalar path, so the config secret registry cannot encrypt them at rest or mask them on read — an admin-written map would sit in Mongo in plaintext and come back in plaintext, widening exposure of what are gateway credentials. Rejects them on both the dotted-patch and object-upsert routes, the same way process-backed MCP servers are held to librechat.yaml. This is what makes "deployment-level" true rather than merely documented. * 🐛 fix: Wire Config Middleware And Header Collisions For Langfuse Two codex review findings. P1 — `api/server/routes/admin/langfuse.js` never mounted `configMiddleware`, so `req.config` was undefined in production and credential verification silently ran without the deployment's proxy headers: exactly the deployments this feature targets could not save a connection. The handler unit tests injected `config` into their mock requests, so they stayed green. Mounts the middleware after the access checks (unauthorized callers still short-circuit first) and adds route-level tests that assert the handler actually receives a resolved config — the composition root, not the component. P2 — spreading custom headers under `Authorization` only replaced an exact-case collision. A configured `authorization` survived alongside the managed `Authorization` and fetch appends rather than replaces, sending both credentials in one combined value. All four request sites now use `mergeHeaders`, which already merges case-insensitively with the override winning; tests cover the lower- and upper-case variants. * 🔒 fix: Mask Langfuse Headers On Read And Harden Value Handling Three codex round-2 findings. P1 — the write guard blocked storing `langfuse.headers` in Mongo but did nothing for the read path: `GET /api/admin/config/base` serves the resolved AppConfig through `redactConfigSecrets`, which only knows registered scalar secrets, so a yaml-configured gateway credential was returned in full to any admin with Langfuse read access. Adds a secret-map registry that masks values while keeping key names, so an admin can still see which headers a deployment sets. Masking is safe precisely because these are yaml-only — a masked read cannot be round-tripped back over the real values. A malformed non-object value at that path is dropped rather than serialized. P2 — `mergeHeaders` indexes one spelling per lowercase name, so a config holding both `authorization` and `AUTHORIZATION` had only one displaced; the survivor was then appended by `Headers` into a combined value. Case variants are now collapsed at resolution, before any consumer sees them. P2 — `resolveHeaders` encodes only values it substitutes a user field into, and no user is supplied here, so a literal or interpolated character above U+00FF reached `Headers` unencoded and threw. Final values now go through `encodeHeaderValue`; Latin-1 still passes verbatim. * 🔒 fix: Keep Langfuse Header Credentials Out Of Logs And Validate Names Three codex round-3 findings, plus a documented boundary for the fourth. P1 — `loadCustomConfig` logs the parsed config at startup (`printConfig` defaults true), so a literal gateway credential in `langfuse.headers` was copied into application logs on every boot, undoing the masking the admin read path had just gained. The printed copy now goes through `redactConfigSecretMaps`, reusing the same registry. Scoped to map-valued secrets so scalar-secret log behavior is unchanged; the live config keeps its real values. P2 — a nonempty but invalid field name (` X-Token`, `X Proxy Token`) passed the emptiness check and then threw in the `Headers` constructor, which would break export, verification, lookup, and feedback for the whole deployment rather than that one header. Names are trimmed and validated against the RFC 7230 token grammar, and dropped with a warning otherwise. P2 — unresolved `${VAR}` detection tested the *resolved* value, so a credential legitimately containing `${...}` was mistaken for a failed substitution and dropped. Detection now inspects the configured text and checks the referenced variables directly, which also drops references to denylisted infrastructure secrets instead of forwarding them verbatim. The fourth (fanout gateway forwards only `Authorization`, so a tenant Langfuse behind its own proxy is not covered) is a real limitation in a separate component. Documented on the schema field and in the example config rather than left implied. * 🔒 fix: Scope Langfuse Headers To Configured Origins Three codex round-4 findings. P1 — one header map was attached to every destination a run resolves to. Under fanout that means a credential meant for an internal gateway was also sent to the central destination, typically Langfuse Cloud: an unrelated third-party origin. Headers are now attached only when the destination's origin is one the deployment explicitly configured (a self-hosted base URL, the fanout collector, or a tenant destination set by env). The built-in `*.cloud.langfuse.com` defaults are excluded precisely because nobody pointed at them. For trace export this also means attaching after the export branch settles on a `baseUrl` rather than before, since which destination wins depends on the branch. P2 — `encodeHeaderValue` only encodes above U+00FF, so a newline, CR, or NUL passed through and threw in `Headers`, breaking every request rather than the one header. Values are trimmed (the common trailing-newline case) then validated against the legal field-value bytes; an embedded CRLF is a request-splitting attempt and is dropped, not stripped. P2 — the write guard matched only the exact `headers` property, so `{ langfuse: { "headers.X-Token": "..." } }` and root-level dotted variants slipped through into the Mixed overrides document, where the nested-map redactor never walks them and a later read returns them in plaintext. All dotted spellings are now rejected. * 🔒 fix: Bind Langfuse Headers To One Configured Origin Four codex round-5 findings. P1 — the round-4 allowlist still authorized every configured origin, so a deployment with both a collector and an explicit central host sent the same credential to both. `langfuse.headers` is one map with no way to say which endpoint it authenticates to, so it is only unambiguous when the deployment configures exactly one Langfuse origin. Iterating on which origins to guess was the wrong axis; headers are now sent only when there is a single configured origin and the destination is it, with a warning when several make the intent unresolvable. That covers the self-hosted case this feature exists for; multi-destination deployments need per-destination headers the schema cannot yet express. P1 — `fetch` defaults to following redirects, and Node strips `Authorization` across origins but keeps arbitrary headers, so a redirect off an allowed origin would hand the gateway credential to a host that passed no check. Requests carrying custom headers now refuse redirects; requests without them keep the default, so nothing changes for existing deployments. P2 — `extractEnvVariable`'s whole-string branch is anchored and greedy, so `${CLIENT_ID}:${CLIENT_SECRET}` parsed as one variable name and the raw template was sent as the credential. References are expanded here now, so only literal values reach that path. P2 — a valid token name is not necessarily usable: `Transfer-Encoding` makes `fetch` throw and a fixed `Content-Length` misdescribes the body of every other request sharing the map. Request-framing names are dropped. * 🐛 fix: Expand Langfuse Header References Exactly Once Codex round 6 (P2). After expanding `${VAR}` references myself I still handed the result to `resolveHeaders`, which runs `extractEnvVariable` over it again — so a credential containing `${PATH}`, or any other name that happens to be set, was silently rewritten on export, verification, lookup, and feedback. The round-3 test only used an *unset* embedded name, which the second pass leaves alone, so it could not catch this. Resolution no longer round-trips through `resolveHeaders`. The only part still wanted from it was stripping `{{...}}` user placeholders, which is now applied directly; expansion, encoding, and validation were already local. Adds a test whose embedded variable is set, which fails against the previous pipeline. * 🐛 fix: Process Langfuse Header Templates Before Substitution Codex round 7 (P2), the mirror of round 6. Having stopped re-expanding the resolved credential, the placeholder strip was still running over it: a token containing `{{LIBRECHAT_USER_ID}}` had that span deleted and `abc{{...}}ghi` went out as `abcghi`. Establishes the invariant the last two rounds were circling. Every template operation — placeholder strip, unresolved-reference check, expansion — now runs on the operator's configured text, and the credential is substituted last and never touched again. Gateway credentials are arbitrary strings, so none of their bytes are syntax. Also moves the unresolved-reference check after the strip, so it no longer reports a variable inside a `{{...}}` span that the strip removes. |
||
|---|---|---|
| .. | ||
| app | ||
| cache | ||
| config | ||
| db | ||
| models | ||
| server | ||
| strategies | ||
| test | ||
| utils | ||
| jest.config.js | ||
| jsconfig.json | ||
| package.json | ||
| typedefs.js | ||