diff --git a/client/src/components/MCP/CustomUserVarsSection.tsx b/client/src/components/MCP/CustomUserVarsSection.tsx
index 051634249e..98e392554a 100644
--- a/client/src/components/MCP/CustomUserVarsSection.tsx
+++ b/client/src/components/MCP/CustomUserVarsSection.tsx
@@ -32,13 +32,14 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
-
+
}
/>
diff --git a/package-lock.json b/package-lock.json
index 102f984482..dede8c7c84 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2236,20 +2236,6 @@
"node": ">= 0.8.0"
}
},
- "api/node_modules/express-rate-limit": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz",
- "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==",
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": "4 || 5 || ^5.0.0-beta.1"
- }
- },
"api/node_modules/express-session": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
@@ -2510,38 +2496,6 @@
}
}
},
- "api/node_modules/mongodb-connection-string-url": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
- "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
- "dependencies": {
- "@types/whatwg-url": "^11.0.2",
- "whatwg-url": "^13.0.0"
- }
- },
- "api/node_modules/mongodb-connection-string-url/node_modules/tr46": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
- "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
- "dependencies": {
- "punycode": "^2.3.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "api/node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
- "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
- "dependencies": {
- "tr46": "^4.1.1",
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=16"
- }
- },
"api/node_modules/mongoose": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz",
@@ -29450,7 +29404,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "dev": true
+ "devOptional": true
},
"node_modules/@types/unist": {
"version": "2.0.10",
@@ -33161,6 +33115,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "peer": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -51507,7 +51471,7 @@
},
"packages/client": {
"name": "@librechat/client",
- "version": "0.2.3",
+ "version": "0.2.4",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.2",
@@ -51560,6 +51524,7 @@
"@tanstack/react-virtual": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dompurify": "^3.2.6",
"framer-motion": "^12.23.6",
"i18next": "^24.2.2 || ^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
diff --git a/packages/client/package.json b/packages/client/package.json
index 0e6a19aeb8..189363f521 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/client",
- "version": "0.2.3",
+ "version": "0.2.4",
"description": "React components for LibreChat",
"main": "dist/index.js",
"module": "dist/index.es.js",
@@ -54,6 +54,7 @@
"@react-spring/web": "^10.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dompurify": "^3.2.6",
"framer-motion": "^12.23.6",
"i18next-browser-languagedetector": "^8.2.0",
"input-otp": "^1.4.2",
diff --git a/packages/client/src/components/Tooltip.css b/packages/client/src/components/Tooltip.css
index 92e52037c4..46ecb37cb8 100644
--- a/packages/client/src/components/Tooltip.css
+++ b/packages/client/src/components/Tooltip.css
@@ -11,6 +11,16 @@
line-height: 1.5rem;
color: black;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25);
+ /* Enhanced layout for longer descriptions */
+ max-width: 320px;
+ word-wrap: break-word;
+ text-align: left;
+}
+
+@media (max-width: 640px) {
+ .tooltip {
+ max-width: 200px;
+ }
}
.tooltip:where(.dark, .dark *) {
diff --git a/packages/client/src/components/Tooltip.tsx b/packages/client/src/components/Tooltip.tsx
index b83452d654..2f7675d10f 100644
--- a/packages/client/src/components/Tooltip.tsx
+++ b/packages/client/src/components/Tooltip.tsx
@@ -1,6 +1,7 @@
+import DOMPurify from 'dompurify';
import * as Ariakit from '@ariakit/react';
+import { forwardRef, useId, useMemo } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
-import { forwardRef, useMemo } from 'react';
import { cn } from '~/utils';
import './Tooltip.css';
@@ -8,18 +9,47 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
className?: string;
- focusable?: boolean;
role?: string;
+ enableHTML?: boolean;
}
export const TooltipAnchor = forwardRef
(function TooltipAnchor(
- { description, side = 'top', className, role, ...props },
+ { description, side = 'top', className, role, enableHTML = false, ...props },
ref,
) {
const tooltip = Ariakit.useTooltipStore({ placement: side });
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
+ const id = useId();
+ const sanitizer = useMemo(() => {
+ const instance = DOMPurify();
+ instance.addHook('afterSanitizeAttributes', (node) => {
+ if (node.tagName && node.tagName === 'A') {
+ node.setAttribute('target', '_blank');
+ node.setAttribute('rel', 'noopener noreferrer');
+ }
+ });
+ return instance;
+ }, []);
+
+ const sanitizedHTML = useMemo(() => {
+ if (!enableHTML) {
+ return '';
+ }
+ try {
+ return sanitizer.sanitize(description, {
+ ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code'],
+ ALLOWED_ATTR: ['href', 'class', 'target', 'rel'],
+ ALLOW_DATA_ATTR: false,
+ ALLOW_ARIA_ATTR: false,
+ });
+ } catch (error) {
+ console.error('Sanitization failed', error);
+ return description;
+ }
+ }, [enableHTML, description, sanitizer]);
+
const { x, y } = useMemo(() => {
const dir = placement.split('-')[0];
switch (dir) {
@@ -49,6 +79,7 @@ export const TooltipAnchor = forwardRef(func
{...props}
ref={ref}
role={role}
+ aria-describedby={id}
onKeyDown={handleKeyDown}
className={cn('cursor-pointer', className)}
/>
@@ -58,6 +89,7 @@ export const TooltipAnchor = forwardRef(func
gutter={4}
alwaysVisible
className="tooltip"
+ id={id}
render={
(func
}
>
- {description}
+ {enableHTML ? (
+
+ ) : (
+ description
+ )}
)}