From fe311df96915ea341dc60568dcc5c348f4c0f8bf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 15 May 2025 12:17:17 -0400 Subject: [PATCH 01/49] =?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 02/49] =?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=" " />