LibreChat/api/server/middleware/denyRequest.js
Marco Beretta 9cbbeb6e85
fix: enforce forced retention on message edits, feedback, and error saves
Two more message-write paths bypassed ephemeral enforcement:

- The edit and feedback endpoints call updateMessage directly, without loading
  retention config, so editing an older permanent message after a switch to
  ephemeral left the message and its conversation non-temporary and visible.
  Load config on those routes and run a new applyForcedRetention helper after the
  update, which stamps the message and cascades the conversation/messages.

- The sendError and denyRequest middleware save messages with retention config
  but never call saveConvo, so a validation/model error or denied-request message
  could outlive its conversation. Pass capExpiryToConversation like the other
  message-only paths.

Extract the conversation cascade into a shared cascadeForcedConversationRetention
helper used by both saveMessage and applyForcedRetention.
2026-06-23 16:41:33 +02:00

70 lines
2.3 KiB
JavaScript

const crypto = require('crypto');
const { sendEvent } = require('@librechat/api');
const { getResponseSender, Constants } = require('librechat-data-provider');
const { sendError } = require('~/server/middleware/error');
const { saveMessage } = require('~/models');
/**
* Denies a request by sending an error message and optionally saves the user's message.
*
* @async
* @function
* @param {Object} req - Express request object.
* @param {Object} req.body - The body of the request.
* @param {string} [req.body.messageId] - The ID of the message.
* @param {string} [req.body.conversationId] - The ID of the conversation.
* @param {string} [req.body.parentMessageId] - The ID of the parent message.
* @param {string} req.body.text - The text of the message.
* @param {Object} res - Express response object.
* @param {string} errorMessage - The error message to be sent.
* @returns {Promise<Object>} A promise that resolves with the error response.
* @throws {Error} Throws an error if there's an issue saving the message or sending the error.
*/
const denyRequest = async (req, res, errorMessage) => {
let responseText = errorMessage;
if (typeof errorMessage === 'object') {
responseText = JSON.stringify(errorMessage);
}
const { messageId, conversationId: _convoId, parentMessageId, text } = req.body;
const conversationId = _convoId ?? crypto.randomUUID();
const userMessage = {
sender: 'User',
messageId: messageId ?? crypto.randomUUID(),
parentMessageId,
conversationId,
isCreatedByUser: true,
text,
};
sendEvent(res, { message: userMessage, created: true });
const shouldSaveMessage = _convoId && parentMessageId && parentMessageId !== Constants.NO_PARENT;
if (shouldSaveMessage) {
await saveMessage(
{
userId: req?.user?.id,
isTemporary: req?.body?.isTemporary,
interfaceConfig: req?.config?.interfaceConfig,
},
{ ...userMessage, user: req.user.id },
{
context: `api/server/middleware/denyRequest.js - ${responseText}`,
capExpiryToConversation: true,
},
);
}
return await sendError(req, res, {
sender: getResponseSender(req.body),
messageId: crypto.randomUUID(),
conversationId,
parentMessageId: userMessage.messageId,
text: responseText,
shouldSaveMessage,
user: req.user.id,
});
};
module.exports = denyRequest;