From f6cc394eabc3298916abfa898b16c5d415bacf0f Mon Sep 17 00:00:00 2001 From: andresgit <9771158+andresgit@users.noreply.github.com> Date: Thu, 15 May 2025 16:35:48 +0300 Subject: [PATCH 01/26] =?UTF-8?q?=F0=9F=8E=A8=20feat:=20add=20`copy-tex`?= =?UTF-8?q?=20to=20improve=20copying=20KaTeX=20(#7308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When selecting equations and using copy paste, uses the correct latex code. Co-authored-by: Ruben Talstra --- client/src/main.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main.jsx b/client/src/main.jsx index 4c7bde0270..53b483f8d2 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -6,6 +6,7 @@ import './style.css'; import './mobile.css'; import { ApiErrorBoundaryProvider } from './hooks/ApiErrorBoundaryContext'; import 'katex/dist/katex.min.css'; +import 'katex/dist/contrib/copy-tex.js'; const container = document.getElementById('root'); const root = createRoot(container); From 621fa6e1aa2330f9d8fa1da6a8706fabbd787202 Mon Sep 17 00:00:00 2001 From: matt burnett Date: Thu, 15 May 2025 09:37:14 -0400 Subject: [PATCH 02/26] =?UTF-8?q?=F0=9F=94=83=20refactor:=20`AgentFooter`?= =?UTF-8?q?=20to=20conditionally=20render=20buttons=20based=20on=20`active?= =?UTF-8?q?Panel`=20(#7306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SidePanel/Agents/AgentFooter.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/client/src/components/SidePanel/Agents/AgentFooter.tsx b/client/src/components/SidePanel/Agents/AgentFooter.tsx index 062f81d253..75f10a3851 100644 --- a/client/src/components/SidePanel/Agents/AgentFooter.tsx +++ b/client/src/components/SidePanel/Agents/AgentFooter.tsx @@ -50,10 +50,12 @@ export default function AgentFooter({ return localize('com_ui_create'); }; + const showButtons = activePanel === Panel.builder; + return (
- {activePanel !== Panel.advanced && } - {user?.role === SystemRoles.ADMIN && } + {showButtons && } + {user?.role === SystemRoles.ADMIN && showButtons && } {/* Context Button */}
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && hasAccessToShareAgents && ( - - )} + + )} {agent && agent.author === user?.id && } {/* Submit Button */}
+ )} +
diff --git a/librechat.example.yaml b/librechat.example.yaml index 0b4963cb2a..ae14b0faae 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -71,6 +71,13 @@ interface: multiConvo: true agents: true +# Example Cloudflare turnstile (optional) +#turnstile: +# siteKey: "your-site-key-here" +# options: +# language: "auto" # "auto" or an ISO 639-1 language code (e.g. en) +# size: "normal" # Options: "normal", "compact", "flexible", or "invisible" + # Example Registration Object Structure (optional) registration: socialLogins: ['github', 'google', 'discord', 'openid', 'facebook', 'apple'] diff --git a/package-lock.json b/package-lock.json index 5aa383170a..f4690d43c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1073,6 +1073,7 @@ "@dicebear/collection": "^9.2.2", "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", + "@marsidev/react-turnstile": "^1.1.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.3", @@ -19729,6 +19730,16 @@ "resolved": "client", "link": true }, + "node_modules/@marsidev/react-turnstile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.1.0.tgz", + "integrity": "sha512-X7bP9ZYutDd+E+klPYF+/BJHqEyyVkN4KKmZcNRr84zs3DcMoftlMAuoKqNSnqg0HE7NQ1844+TLFSJoztCdSA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.2 || ^18.0.0 || ^19.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" + } + }, "node_modules/@microsoft/eslint-formatter-sarif": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@microsoft/eslint-formatter-sarif/-/eslint-formatter-sarif-3.1.0.tgz", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 978aea2520..bdac70a0c6 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -505,10 +505,28 @@ export const intefaceSchema = z export type TInterfaceConfig = z.infer; export type TBalanceConfig = z.infer; +export const turnstileOptionsSchema = z + .object({ + language: z.string().default('auto'), + size: z.enum(['normal', 'compact', 'flexible', 'invisible']).default('normal'), + }) + .default({ + language: 'auto', + size: 'normal', + }); + +export const turnstileSchema = z.object({ + siteKey: z.string(), + options: turnstileOptionsSchema.optional(), +}); + +export type TTurnstileConfig = z.infer; + export type TStartupConfig = { appTitle: string; socialLogins?: string[]; interface?: TInterfaceConfig; + turnstile?: TTurnstileConfig; balance?: TBalanceConfig; discordLoginEnabled: boolean; facebookLoginEnabled: boolean; @@ -578,6 +596,7 @@ export const configSchema = z.object({ filteredTools: z.array(z.string()).optional(), mcpServers: MCPServersSchema.optional(), interface: intefaceSchema, + turnstile: turnstileSchema.optional(), fileStrategy: fileSourceSchema.default(FileSources.local), actions: z .object({ From fe311df96915ea341dc60568dcc5c348f4c0f8bf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 15 May 2025 12:17:17 -0400 Subject: [PATCH 04/26] =?UTF-8?q?=F0=9F=94=84=20fix:=20Improve=20MCP=20Con?= =?UTF-8?q?nection=20Cleanup=20(#7400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: linting for mcp related modules * fix: update `isConnected` method to return a Promise and handle connection state asynchronously to properly handle/cleanup disconnected user connections --- packages/data-provider/src/mcp.ts | 7 +++++-- packages/mcp/src/connection.ts | 10 ++++++++-- packages/mcp/src/manager.ts | 14 +++++++------- packages/mcp/src/parsers.ts | 10 +++++++++- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index 1fdb6c95f6..8f406fd391 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -85,7 +85,10 @@ export const SSEOptionsSchema = BaseOptionsSchema.extend({ export const StreamableHTTPOptionsSchema = BaseOptionsSchema.extend({ type: z.literal('streamable-http'), headers: z.record(z.string(), z.string()).optional(), - url: z.string().url().refine( + url: z + .string() + .url() + .refine( (val) => { const protocol = new URL(val).protocol; return protocol !== 'ws:' && protocol !== 'wss:'; @@ -93,7 +96,7 @@ export const StreamableHTTPOptionsSchema = BaseOptionsSchema.extend({ { message: 'Streamable HTTP URL must not start with ws:// or wss://', }, - ), + ), }); export const MCPOptionsSchema = z.union([ diff --git a/packages/mcp/src/connection.ts b/packages/mcp/src/connection.ts index 23b7062dd1..aff0e080ad 100644 --- a/packages/mcp/src/connection.ts +++ b/packages/mcp/src/connection.ts @@ -567,8 +567,14 @@ export class MCPConnection extends EventEmitter { return this.connectionState; } - public isConnected(): boolean { - return this.connectionState === 'connected'; + public async isConnected(): Promise { + try { + await this.client.ping(); + return this.connectionState === 'connected'; + } catch (error) { + this.logger?.error(`${this.getLogPrefix()} Ping failed:`, error); + return false; + } } public getLastError(): Error | null { diff --git a/packages/mcp/src/manager.ts b/packages/mcp/src/manager.ts index df74580994..8fe9074b8f 100644 --- a/packages/mcp/src/manager.ts +++ b/packages/mcp/src/manager.ts @@ -71,7 +71,7 @@ export class MCPManager { const connectionAttempt = this.initializeServer(connection, `[MCP][${serverName}]`); await Promise.race([connectionAttempt, connectionTimeout]); - if (connection.isConnected()) { + if (await connection.isConnected()) { initializedServers.add(i); this.connections.set(serverName, connection); // Store in app-level map @@ -135,7 +135,7 @@ export class MCPManager { while (attempts < maxAttempts) { try { await connection.connect(); - if (connection.isConnected()) { + if (await connection.isConnected()) { return; } throw new Error('Connection attempt succeeded but status is not connected'); @@ -200,7 +200,7 @@ export class MCPManager { } connection = undefined; // Force creation of a new connection } else if (connection) { - if (connection.isConnected()) { + if (await connection.isConnected()) { this.logger.debug(`[MCP][User: ${userId}][${serverName}] Reusing active connection`); // Update timestamp on reuse this.updateUserLastActivity(userId); @@ -244,7 +244,7 @@ export class MCPManager { ); await Promise.race([connectionAttempt, connectionTimeout]); - if (!connection.isConnected()) { + if (!(await connection.isConnected())) { throw new Error('Failed to establish connection after initialization attempt.'); } @@ -342,7 +342,7 @@ export class MCPManager { public async mapAvailableTools(availableTools: t.LCAvailableTools): Promise { for (const [serverName, connection] of this.connections.entries()) { try { - if (connection.isConnected() !== true) { + if ((await connection.isConnected()) !== true) { this.logger.warn( `[MCP][${serverName}] Connection not established. Skipping tool mapping.`, ); @@ -375,7 +375,7 @@ export class MCPManager { for (const [serverName, connection] of this.connections.entries()) { try { - if (connection.isConnected() !== true) { + if ((await connection.isConnected()) !== true) { this.logger.warn( `[MCP][${serverName}] Connection not established. Skipping manifest loading.`, ); @@ -443,7 +443,7 @@ export class MCPManager { } } - if (!connection.isConnected()) { + if (!(await connection.isConnected())) { // This might happen if getUserConnection failed silently or app connection dropped throw new McpError( ErrorCode.InternalError, // Use InternalError for connection issues diff --git a/packages/mcp/src/parsers.ts b/packages/mcp/src/parsers.ts index b77c7efa19..3350148a59 100644 --- a/packages/mcp/src/parsers.ts +++ b/packages/mcp/src/parsers.ts @@ -1,5 +1,13 @@ import type * as t from './types/mcp'; -const RECOGNIZED_PROVIDERS = new Set(['google', 'anthropic', 'openai', 'openrouter', 'xai', 'deepseek', 'ollama']); +const RECOGNIZED_PROVIDERS = new Set([ + 'google', + 'anthropic', + 'openai', + 'openrouter', + 'xai', + 'deepseek', + 'ollama', +]); const CONTENT_ARRAY_PROVIDERS = new Set(['google', 'anthropic', 'openai']); const imageFormatters: Record = { From 7a91f6ca62865b0e6b94b816e59f0f39382a9832 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Thu, 15 May 2025 22:25:10 +0200 Subject: [PATCH 05/26] =?UTF-8?q?=F0=9F=94=92=20feat:=20Add=20Content=20Se?= =?UTF-8?q?curity=20Policy=20using=20Helmet=20middleware=20(#7377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 feat: Add Content Security Policy using Helmet middleware * 🔒 feat: Set trust proxy and refine Content Security Policy directives * 🎨 feat: add `copy-tex` to improve copying KaTeX (#7308) When selecting equations and using copy paste, uses the correct latex code. Co-authored-by: Ruben Talstra * 🔃 refactor: `AgentFooter` to conditionally render buttons based on `activePanel` (#7306) * 🚀 feat: Add `Cloudflare Turnstile` support (#5987) * 🚀 feat: Add @marsidev/react-turnstile dependency to package.json and package-lock.json * 🚀 feat: Integrate Cloudflare Turnstile configuration support in AppService and add schema validation * 🚀 feat: Implemented Cloudflare Turnstile integration in Login and Registration forms * 🚀 feat: Enhance AppService tests with additional mocks and configuration setups * 🚀 feat: Comment out outdated config version warning tests in AppService.spec.js * 🚀 feat: Remove outdated warning tests and add new checks for environment variables and API health * 🔧 test: Update AppService.spec.js to use expect.anything() for paths validation * 🔧 test: Refactor AppService.spec.js to streamline mocks and enhance clarity * 🔧 chore: removed not needed test * Potential fix for code scanning alert no. 5638: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5629: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5642: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update turnstile.js * Potential fix for code scanning alert no. 5634: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5646: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5647: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * 🔒 feat: Refactor Content Security Policy setup to use Helmet middleware with custom directives * 🔒 feat: Enhance Content Security Policy to include Sandpack Bundler URL * 🔒 feat: Update Content Security Policy and integrate Turnstile captcha support --------- Co-authored-by: andresgit <9771158+andresgit@users.noreply.github.com> Co-authored-by: matt burnett Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- api/package.json | 1 + api/server/index.js | 62 +++++++++++++++++---- api/server/services/start/turnstile.js | 11 +++- client/src/components/Auth/LoginForm.tsx | 42 ++++---------- client/src/components/Auth/Registration.tsx | 27 +++------ package-lock.json | 10 ++++ 6 files changed, 93 insertions(+), 60 deletions(-) diff --git a/api/package.json b/api/package.json index bcf94a6cad..8782457dbc 100644 --- a/api/package.json +++ b/api/package.json @@ -71,6 +71,7 @@ "firebase": "^11.0.2", "googleapis": "^126.0.1", "handlebars": "^4.7.7", + "helmet": "^8.1.0", "https-proxy-agent": "^7.0.6", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", diff --git a/api/server/index.js b/api/server/index.js index cd0bdd3f88..3c8d3dd951 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -2,6 +2,7 @@ require('dotenv').config(); const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..') }); const cors = require('cors'); +const helmet = require('helmet'); const axios = require('axios'); const express = require('express'); const compression = require('compression'); @@ -22,7 +23,15 @@ const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); const routes = require('./routes'); -const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; +const { + PORT, + HOST, + ALLOW_SOCIAL_LOGIN, + DISABLE_COMPRESSION, + TRUST_PROXY, + SANDPACK_BUNDLER_URL, + SANDPACK_STATIC_BUNDLER_URL, +} = process.env ?? {}; const port = Number(PORT) || 3080; const host = HOST || 'localhost'; @@ -38,6 +47,8 @@ const startServer = async () => { const app = express(); app.disable('x-powered-by'); + app.set('trust proxy', trusted_proxy); + await AppService(app); const indexPath = path.join(app.locals.paths.dist, 'index.html'); @@ -49,23 +60,54 @@ const startServer = async () => { app.use(noIndex); app.use(errorController); app.use(express.json({ limit: '3mb' })); - app.use(mongoSanitize()); app.use(express.urlencoded({ extended: true, limit: '3mb' })); - app.use(staticCache(app.locals.paths.dist)); - app.use(staticCache(app.locals.paths.fonts)); - app.use(staticCache(app.locals.paths.assets)); - app.set('trust proxy', trusted_proxy); + app.use(mongoSanitize()); app.use(cors()); app.use(cookieParser()); + app.use( + helmet({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", 'https://challenges.cloudflare.com'], + styleSrc: ["'self'", "'unsafe-inline'"], + fontSrc: ["'self'", 'data:'], + objectSrc: ["'none'"], + imgSrc: ["'self'", 'data:'], + mediaSrc: ["'self'", 'data:', 'blob:'], + connectSrc: ["'self'"], + frameSrc: [ + "'self'", + 'https://challenges.cloudflare.com', + 'https://codesandbox.io', + ...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []), + ...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []), + ], + frameAncestors: [ + "'self'", + 'https://codesandbox.io', + ...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []), + ...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []), + ], + }, + }, + }), + ); if (!isEnabled(DISABLE_COMPRESSION)) { app.use(compression()); + } else { + console.warn('Response compression has been disabled via DISABLE_COMPRESSION.'); } + // Serve static assets with aggressive caching + app.use(staticCache(app.locals.paths.dist)); + app.use(staticCache(app.locals.paths.fonts)); + app.use(staticCache(app.locals.paths.assets)); + if (!ALLOW_SOCIAL_LOGIN) { - console.warn( - 'Social logins are disabled. Set Environment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.', - ); + console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.'); } /* OAUTH */ @@ -128,7 +170,7 @@ const startServer = async () => { }); app.listen(port, host, () => { - if (host == '0.0.0.0') { + if (host === '0.0.0.0') { logger.info( `Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`, ); diff --git a/api/server/services/start/turnstile.js b/api/server/services/start/turnstile.js index ffd4545dae..be9e5f83c7 100644 --- a/api/server/services/start/turnstile.js +++ b/api/server/services/start/turnstile.js @@ -26,7 +26,16 @@ function loadTurnstileConfig(config, configDefaults) { options: customTurnstile.options ?? defaults.options, }); - logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2)); + const enabled = Boolean(loadedTurnstile.siteKey); + + if (enabled) { + logger.info( + 'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2), + ); + } else { + logger.info('Turnstile is DISABLED (no siteKey provided).'); + } + return loadedTurnstile; } diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 030b6323f7..e34ec3c94d 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -16,6 +16,7 @@ type TLoginFormProps = { const LoginForm: React.FC = ({ onSubmit, startupConfig, error, setError }) => { const localize = useLocalize(); const { theme } = useContext(ThemeContext); + const { register, getValues, @@ -28,6 +29,7 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, const { data: config } = useGetStartupConfig(); const useUsernameLogin = config?.ldap?.username; const validTheme = theme === 'dark' ? 'dark' : 'light'; + const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey); useEffect(() => { if (error && error.includes('422') && !showResendLink) { @@ -100,20 +102,12 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, }, })} aria-invalid={!!errors.email} - className=" - webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light - bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none - " + className="peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary transition-colors duration-200 focus:border-green-500 focus:outline-none" placeholder=" " />