mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-08 15:41:38 +00:00
* ⚡ perf: Stop Awaiting the Conversation Access Marker Write Without Redis the CONVO_ACCESS violations namespace is backed by keyv-file, whose debounced write resolves after ~100ms. validateConvoAccess awaited that write before calling next(), so the first message to any existing conversation waited ~100ms before the request was even admitted — once per conversation per ten-minute window, on every default deployment. The marker only short-circuits the next check, so the write no longer gates the request. The same read now stashes the full document on req.resolvedConversation (null when absent) for downstream consumers. First-turn ack on an existing conversation: 109ms -> 5ms. * ⚡ perf: Read the Conversation Once per Chat Turn A chat turn read the same conversation document four times: the access check (two fields), the subagent thread guard (full document), agent initialization (the files field), and the first save. The access check now reads the full document and leaves it on req.resolvedConversation, the guard accepts that pre-resolved document instead of re-reading, and initializeAgent takes the conversation's file refs from it rather than issuing a separate findOne. Two serial round trips removed from every turn; the same document still serves the first save as before. * ⚡ perf: Remove Duplicate JWT Authentication on Agents Routes routes/agents/index.js applies requireJwtAuth and then mounts the v1 router at '/', which applied requireJwtAuth again. Every request through the agents router — chat turns included — ran the passport strategy twice: two signature checks and two user document reads. The v1 router is mounted nowhere else; its separately exported avatar router carries its own auth in files/index.js. * ⚡ perf: Skip the History Read for Root-Parent Turns and Walk the Tree in O(n) loadHistory fetched every message in the conversation and then walked the parent chain from the request's head. For a new conversation — or a new branch from the root of an existing one — the head is the root sentinel, which no message carries as its id, so the walk was empty by construction and the fetch was wasted. It now returns early. getMessagesForConversation found each ancestor with Array.find inside the walk, O(n^2) on a linear conversation (~5ms at 1000 messages). A Map by messageId makes it O(n); first-match semantics are preserved.
92 lines
3.4 KiB
JavaScript
92 lines
3.4 KiB
JavaScript
const { isEnabled } = require('@librechat/api');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { Constants, ViolationTypes, Time } = require('librechat-data-provider');
|
|
const denyRequest = require('~/server/middleware/denyRequest');
|
|
const { logViolation, getLogStores } = require('~/cache');
|
|
const { searchConversation } = require('~/models');
|
|
|
|
const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {};
|
|
|
|
/**
|
|
* Helper function to get conversationId from different request body structures.
|
|
* @param {Object} body - The request body.
|
|
* @returns {string|undefined} The conversationId.
|
|
*/
|
|
const getConversationId = (body) => {
|
|
return body.conversationId ?? body.arg?.conversationId;
|
|
};
|
|
|
|
/**
|
|
* Middleware to validate user's authorization for a conversation.
|
|
*
|
|
* This middleware checks if a user has the right to access a specific conversation.
|
|
* If the user doesn't have access, an error is returned. If the conversation doesn't exist,
|
|
* a not found error is returned. If the access is valid, the middleware allows the request to proceed.
|
|
* If the `cache` store is not available, the middleware will skip its logic.
|
|
*
|
|
* @function
|
|
* @param {ServerRequest} req - Express request object containing user information.
|
|
* @param {Express.Response} res - Express response object.
|
|
* @param {function} next - Express next middleware function.
|
|
* @throws {Error} Throws an error if the user doesn't have access to the conversation.
|
|
*/
|
|
const validateConvoAccess = async (req, res, next) => {
|
|
const namespace = ViolationTypes.CONVO_ACCESS;
|
|
const cache = getLogStores(namespace);
|
|
|
|
const conversationId = getConversationId(req.body);
|
|
|
|
if (!conversationId || conversationId === Constants.NEW_CONVO) {
|
|
return next();
|
|
}
|
|
|
|
const userId = req.user?.id ?? req.user?._id ?? '';
|
|
const type = ViolationTypes.CONVO_ACCESS;
|
|
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId}:${conversationId}`;
|
|
|
|
try {
|
|
if (cache) {
|
|
const cachedAccess = await cache.get(key);
|
|
if (cachedAccess === 'authorized') {
|
|
return next();
|
|
}
|
|
}
|
|
|
|
/** One read serves the subagent guard, agent initialization, and the first save via
|
|
* `req.resolvedConversation`. `messages` is the only unbounded field and no consumer
|
|
* reads it, so it stays excluded — ownership is not yet known at this point. */
|
|
const conversation = await searchConversation(conversationId, '-messages');
|
|
|
|
if (!conversation) {
|
|
req.resolvedConversation = null;
|
|
return next();
|
|
}
|
|
|
|
if (conversation.user !== userId) {
|
|
const errorMessage = {
|
|
type,
|
|
error: 'User not authorized for this conversation',
|
|
};
|
|
|
|
if (cache) {
|
|
await logViolation(req, res, type, errorMessage, score);
|
|
}
|
|
return await denyRequest(req, res, errorMessage);
|
|
}
|
|
|
|
if (cache) {
|
|
/** The marker only short-circuits the next check; the violations store is file-backed
|
|
* without Redis and its debounced write takes ~100ms, so it must not gate this request. */
|
|
cache.set(key, 'authorized', Time.TEN_MINUTES).catch((error) => {
|
|
logger.warn('[validateConvoAccess] Failed to cache conversation access', error);
|
|
});
|
|
}
|
|
req.resolvedConversation = conversation;
|
|
next();
|
|
} catch (error) {
|
|
console.error('Error validating conversation access:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
};
|
|
|
|
module.exports = validateConvoAccess;
|