chore: import upstream snapshot with attribution
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
This commit is contained in:
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { isEnabled, math, removePorts } = require('@librechat/api');
|
||||
const { deleteAllUserSessions } = require('~/models');
|
||||
const getLogStores = require('./getLogStores');
|
||||
|
||||
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
|
||||
const interval = math(BAN_INTERVAL, 20);
|
||||
|
||||
/**
|
||||
* Bans a user based on violation criteria.
|
||||
*
|
||||
* If the user's violation count is a multiple of the BAN_INTERVAL, the user will be banned.
|
||||
* The duration of the ban is determined by the BAN_DURATION environment variable.
|
||||
* If BAN_DURATION is not set or invalid, the user will not be banned.
|
||||
* Sessions will be deleted and the refreshToken cookie will be cleared even with
|
||||
* an invalid or nill duration, which is a "soft" ban; the user can remain active until
|
||||
* access token expiry.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} req - Express request object containing user information.
|
||||
* @param {Object} res - Express response object.
|
||||
* @param {Object} errorMessage - Object containing user violation details.
|
||||
* @param {string} errorMessage.type - Type of the violation.
|
||||
* @param {string} errorMessage.user_id - ID of the user who committed the violation.
|
||||
* @param {number} errorMessage.violation_count - Number of violations committed by the user.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
*/
|
||||
const banViolation = async (req, res, errorMessage) => {
|
||||
if (!isEnabled(BAN_VIOLATIONS)) {
|
||||
return;
|
||||
}
|
||||
if (!errorMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, user_id, prev_count, violation_count } = errorMessage;
|
||||
|
||||
const prevThreshold = Math.floor(prev_count / interval);
|
||||
const currentThreshold = Math.floor(violation_count / interval);
|
||||
|
||||
if (prevThreshold >= currentThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteAllUserSessions({ userId: user_id });
|
||||
|
||||
/** Clear OpenID session tokens if present */
|
||||
if (req.session?.openidTokens) {
|
||||
delete req.session.openidTokens;
|
||||
}
|
||||
|
||||
res.clearCookie('refreshToken');
|
||||
res.clearCookie('openid_access_token');
|
||||
res.clearCookie('openid_id_token');
|
||||
res.clearCookie('openid_user_id');
|
||||
res.clearCookie('token_provider');
|
||||
|
||||
const banLogs = getLogStores(ViolationTypes.BAN);
|
||||
const duration = errorMessage.duration || banLogs.opts.ttl;
|
||||
if (duration <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
req.ip = removePorts(req);
|
||||
logger.info(
|
||||
`[BAN] Banning user ${user_id} ${req.ip ? `@ ${req.ip} ` : ''}for ${
|
||||
duration / 1000 / 60
|
||||
} minutes`,
|
||||
);
|
||||
|
||||
const expiresAt = Date.now() + duration;
|
||||
await banLogs.set(user_id, { type, violation_count, duration, expiresAt });
|
||||
if (req.ip) {
|
||||
await banLogs.set(req.ip, { type, user_id, violation_count, duration, expiresAt });
|
||||
}
|
||||
|
||||
errorMessage.ban = true;
|
||||
errorMessage.ban_duration = duration;
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
module.exports = banViolation;
|
||||
Vendored
+136
@@ -0,0 +1,136 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const banViolation = require('./banViolation');
|
||||
|
||||
// Mock deleteAllUserSessions since we're testing ban logic, not session deletion
|
||||
jest.mock('~/models', () => ({
|
||||
...jest.requireActual('~/models'),
|
||||
deleteAllUserSessions: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
describe('banViolation', () => {
|
||||
let mongoServer;
|
||||
let req, res, errorMessage;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create();
|
||||
const mongoUri = mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
ip: '127.0.0.1',
|
||||
cookies: {
|
||||
refreshToken: 'someToken',
|
||||
},
|
||||
};
|
||||
res = {
|
||||
clearCookie: jest.fn(),
|
||||
};
|
||||
errorMessage = {
|
||||
type: 'someViolation',
|
||||
user_id: new mongoose.Types.ObjectId().toString(), // Use valid ObjectId
|
||||
prev_count: 0,
|
||||
violation_count: 0,
|
||||
};
|
||||
process.env.BAN_VIOLATIONS = 'true';
|
||||
process.env.BAN_DURATION = '7200000'; // 2 hours in ms
|
||||
process.env.BAN_INTERVAL = '20';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not ban if BAN_VIOLATIONS are not enabled', async () => {
|
||||
process.env.BAN_VIOLATIONS = 'false';
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not ban if errorMessage is not provided', async () => {
|
||||
await banViolation(req, res, null);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
|
||||
it('[1/3] should ban if violation_count crosses the interval threshold: 19 -> 39', async () => {
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = 39;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeTruthy();
|
||||
});
|
||||
|
||||
it('[2/3] should ban if violation_count crosses the interval threshold: 19 -> 20', async () => {
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = 20;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeTruthy();
|
||||
});
|
||||
|
||||
const randomValueAbove = Math.floor(20 + Math.random() * 100);
|
||||
it(`[3/3] should ban if violation_count crosses the interval threshold: 19 -> ${randomValueAbove}`, async () => {
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = randomValueAbove;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle invalid BAN_INTERVAL and default to 20', async () => {
|
||||
process.env.BAN_INTERVAL = 'invalid';
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = 39;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should ban if BAN_DURATION is invalid as default is 2 hours', async () => {
|
||||
process.env.BAN_DURATION = 'invalid';
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = 39;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not ban if BAN_DURATION is 0 but should clear cookies', async () => {
|
||||
process.env.BAN_DURATION = '0';
|
||||
errorMessage.prev_count = 19;
|
||||
errorMessage.violation_count = 39;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(res.clearCookie).toHaveBeenCalledWith('refreshToken');
|
||||
});
|
||||
|
||||
it('should not ban if violation_count does not change', async () => {
|
||||
errorMessage.prev_count = 0;
|
||||
errorMessage.violation_count = 0;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
|
||||
it('[1/2] should not ban if violation_count does not cross the interval threshold: 0 -> 19', async () => {
|
||||
errorMessage.prev_count = 0;
|
||||
errorMessage.violation_count = 19;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
|
||||
const randomValueUnder = Math.floor(1 + Math.random() * 19);
|
||||
it(`[2/2] should not ban if violation_count does not cross the interval threshold: 0 -> ${randomValueUnder}`, async () => {
|
||||
errorMessage.prev_count = 0;
|
||||
errorMessage.violation_count = randomValueUnder;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
|
||||
it('[EDGE CASE] should not ban if violation_count is lower', async () => {
|
||||
errorMessage.prev_count = 0;
|
||||
errorMessage.violation_count = -10;
|
||||
await banViolation(req, res, errorMessage);
|
||||
expect(errorMessage.ban).toBeFalsy();
|
||||
});
|
||||
});
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { Time, CacheKeys } = require('librechat-data-provider');
|
||||
const getLogStores = require('./getLogStores');
|
||||
|
||||
const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {};
|
||||
|
||||
/**
|
||||
* Clear or decrement pending requests from the cache.
|
||||
* Checks the environmental variable LIMIT_CONCURRENT_MESSAGES;
|
||||
* if the rule is enabled ('true'), it either decrements the count of pending requests
|
||||
* or deletes the key if the count is less than or equal to 1.
|
||||
*
|
||||
* @module clearPendingReq
|
||||
* @requires ./getLogStores
|
||||
* @requires ../server/utils
|
||||
* @requires process
|
||||
*
|
||||
* @async
|
||||
* @function
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The user ID for which the pending requests are to be cleared or decremented.
|
||||
* @param {Object} [params.cache] - An optional cache object to use. If not provided, a default cache will be fetched using getLogStores.
|
||||
* @returns {Promise<void>} A promise that either decrements the 'pendingRequests' count, deletes the key from the store, or resolves with no value.
|
||||
*/
|
||||
const clearPendingReq = async ({ userId, cache: _cache }) => {
|
||||
if (!userId) {
|
||||
return;
|
||||
} else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = CacheKeys.PENDING_REQ;
|
||||
const cache = _cache ?? getLogStores(namespace);
|
||||
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId ?? ''}`;
|
||||
const currentReq = +((await cache.get(key)) ?? 0);
|
||||
|
||||
if (currentReq && currentReq >= 1) {
|
||||
await cache.set(key, currentReq - 1, Time.ONE_MINUTE);
|
||||
} else {
|
||||
await cache.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = clearPendingReq;
|
||||
Vendored
+236
@@ -0,0 +1,236 @@
|
||||
const { Keyv } = require('keyv');
|
||||
const { Time, CacheKeys, ViolationTypes } = require('librechat-data-provider');
|
||||
const {
|
||||
logFile,
|
||||
keyvMongo,
|
||||
cacheConfig,
|
||||
sessionCache,
|
||||
standardCache,
|
||||
violationCache,
|
||||
userPrincipalsCache,
|
||||
registerShutdownTask,
|
||||
} = require('@librechat/api');
|
||||
|
||||
/** No-op store registered when the user principals cache is disabled (TTL of 0). */
|
||||
const disabledCache = {
|
||||
get: async () => undefined,
|
||||
set: async () => undefined,
|
||||
delete: async () => undefined,
|
||||
clear: async () => undefined,
|
||||
};
|
||||
|
||||
const namespaces = {
|
||||
[ViolationTypes.GENERAL]: new Keyv({ store: logFile, namespace: 'violations' }),
|
||||
[ViolationTypes.LOGINS]: violationCache(ViolationTypes.LOGINS),
|
||||
[ViolationTypes.CONCURRENT]: violationCache(ViolationTypes.CONCURRENT),
|
||||
[ViolationTypes.NON_BROWSER]: violationCache(ViolationTypes.NON_BROWSER),
|
||||
[ViolationTypes.MESSAGE_LIMIT]: violationCache(ViolationTypes.MESSAGE_LIMIT),
|
||||
[ViolationTypes.REGISTRATIONS]: violationCache(ViolationTypes.REGISTRATIONS),
|
||||
[ViolationTypes.TOKEN_BALANCE]: violationCache(ViolationTypes.TOKEN_BALANCE),
|
||||
[ViolationTypes.TTS_LIMIT]: violationCache(ViolationTypes.TTS_LIMIT),
|
||||
[ViolationTypes.STT_LIMIT]: violationCache(ViolationTypes.STT_LIMIT),
|
||||
[ViolationTypes.CONVO_ACCESS]: violationCache(ViolationTypes.CONVO_ACCESS),
|
||||
[ViolationTypes.TOOL_CALL_LIMIT]: violationCache(ViolationTypes.TOOL_CALL_LIMIT),
|
||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: violationCache(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.VERIFY_EMAIL_LIMIT]: violationCache(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
||||
[ViolationTypes.RESET_PASSWORD_LIMIT]: violationCache(ViolationTypes.RESET_PASSWORD_LIMIT),
|
||||
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: violationCache(ViolationTypes.ILLEGAL_MODEL_REQUEST),
|
||||
[ViolationTypes.BAN]: new Keyv({
|
||||
store: keyvMongo,
|
||||
namespace: CacheKeys.BANS,
|
||||
ttl: cacheConfig.BAN_DURATION,
|
||||
}),
|
||||
|
||||
[CacheKeys.OPENID_SESSION]: sessionCache(CacheKeys.OPENID_SESSION),
|
||||
[CacheKeys.SAML_SESSION]: sessionCache(CacheKeys.SAML_SESSION),
|
||||
|
||||
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
||||
[CacheKeys.USER_PRINCIPALS]: userPrincipalsCache() ?? disabledCache,
|
||||
[CacheKeys.APP_CONFIG]: standardCache(CacheKeys.APP_CONFIG),
|
||||
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
||||
[CacheKeys.TOOL_CACHE]: standardCache(CacheKeys.TOOL_CACHE),
|
||||
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
|
||||
[CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }),
|
||||
[CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES),
|
||||
[CacheKeys.TOKEN_CONFIG]: standardCache(CacheKeys.TOKEN_CONFIG, Time.THIRTY_MINUTES),
|
||||
[CacheKeys.GEN_TITLE]: standardCache(CacheKeys.GEN_TITLE, Time.TWO_MINUTES),
|
||||
[CacheKeys.S3_EXPIRY_INTERVAL]: standardCache(CacheKeys.S3_EXPIRY_INTERVAL, Time.THIRTY_MINUTES),
|
||||
[CacheKeys.MODEL_QUERIES]: standardCache(CacheKeys.MODEL_QUERIES),
|
||||
[CacheKeys.AUDIO_RUNS]: standardCache(CacheKeys.AUDIO_RUNS, Time.TEN_MINUTES),
|
||||
[CacheKeys.MESSAGES]: standardCache(CacheKeys.MESSAGES, Time.ONE_MINUTE),
|
||||
[CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 10),
|
||||
[CacheKeys.OPENID_EXCHANGED_TOKENS]: standardCache(
|
||||
CacheKeys.OPENID_EXCHANGED_TOKENS,
|
||||
Time.TEN_MINUTES,
|
||||
),
|
||||
[CacheKeys.ADMIN_OAUTH_EXCHANGE]: standardCache(
|
||||
CacheKeys.ADMIN_OAUTH_EXCHANGE,
|
||||
Time.THIRTY_SECONDS,
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all cache stores that have TTL configured
|
||||
* @returns {Keyv[]}
|
||||
*/
|
||||
function getTTLStores() {
|
||||
return Object.values(namespaces).filter(
|
||||
(store) =>
|
||||
store instanceof Keyv &&
|
||||
parseInt(store.opts?.ttl ?? '0') > 0 &&
|
||||
!store.opts?.store?.constructor?.name?.includes('Redis'), // Only include non-Redis stores
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears entries older than the cache's TTL
|
||||
* @param {Keyv} cache
|
||||
*/
|
||||
async function clearExpiredFromCache(cache) {
|
||||
if (!cache?.opts?.store?.entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ttl = cache.opts.ttl;
|
||||
if (!ttl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiryTime = Date.now() - ttl;
|
||||
let cleared = 0;
|
||||
|
||||
// Get all keys first to avoid modification during iteration
|
||||
const keys = Array.from(cache.opts.store.keys());
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const raw = cache.opts.store.get(key);
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = cache.opts.deserialize(raw);
|
||||
// Check if the entry is older than TTL
|
||||
if (data?.expires && data.expires <= expiryTime) {
|
||||
const deleted = await cache.opts.store.delete(key);
|
||||
if (!deleted) {
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
||||
continue;
|
||||
}
|
||||
cleared++;
|
||||
}
|
||||
} catch (error) {
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.log(`[Cache] Error processing entry from ${cache.opts.namespace}:`, error);
|
||||
const deleted = await cache.opts.store.delete(key);
|
||||
if (!deleted) {
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
||||
continue;
|
||||
}
|
||||
cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.log(
|
||||
`[Cache] Cleared ${cleared} entries older than ${ttl}ms from ${cache.opts.namespace}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const auditCache = () => {
|
||||
const ttlStores = getTTLStores();
|
||||
console.log('[Cache] Starting audit');
|
||||
|
||||
ttlStores.forEach((store) => {
|
||||
if (!store?.opts?.store?.entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Cache] ${store.opts.namespace} entries:`, {
|
||||
count: store.opts.store.size,
|
||||
ttl: store.opts.ttl,
|
||||
keys: Array.from(store.opts.store.keys()),
|
||||
entriesWithTimestamps: Array.from(store.opts.store.entries()).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears expired entries from all TTL-enabled stores
|
||||
*/
|
||||
async function clearAllExpiredFromCache() {
|
||||
const ttlStores = getTTLStores();
|
||||
await Promise.all(ttlStores.map((store) => clearExpiredFromCache(store)));
|
||||
|
||||
// Force garbage collection if available (Node.js with --expose-gc flag)
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
}
|
||||
|
||||
if (!cacheConfig.USE_REDIS && !cacheConfig.CI) {
|
||||
/** @type {Set<NodeJS.Timeout>} */
|
||||
const cleanupIntervals = new Set();
|
||||
|
||||
// Clear expired entries every 30 seconds
|
||||
const cleanup = setInterval(() => {
|
||||
clearAllExpiredFromCache();
|
||||
}, Time.THIRTY_SECONDS);
|
||||
|
||||
cleanupIntervals.add(cleanup);
|
||||
|
||||
if (cacheConfig.DEBUG_MEMORY_CACHE) {
|
||||
const monitor = setInterval(() => {
|
||||
const ttlStores = getTTLStores();
|
||||
const memory = process.memoryUsage();
|
||||
const totalSize = ttlStores.reduce((sum, store) => sum + (store.opts?.store?.size ?? 0), 0);
|
||||
|
||||
console.log('[Cache] Memory usage:', {
|
||||
heapUsed: `${(memory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||
heapTotal: `${(memory.heapTotal / 1024 / 1024).toFixed(2)} MB`,
|
||||
rss: `${(memory.rss / 1024 / 1024).toFixed(2)} MB`,
|
||||
external: `${(memory.external / 1024 / 1024).toFixed(2)} MB`,
|
||||
totalCacheEntries: totalSize,
|
||||
});
|
||||
|
||||
auditCache();
|
||||
}, Time.ONE_MINUTE);
|
||||
|
||||
cleanupIntervals.add(monitor);
|
||||
}
|
||||
|
||||
// Register cleanup with the centralized graceful-shutdown coordinator
|
||||
// (see packages/api/src/app/shutdown.ts) rather than attaching a direct
|
||||
// signal handler — multiple competing handlers race the HTTP drain.
|
||||
registerShutdownTask('cache cleanup', async () => {
|
||||
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Cleaning up and shutting down...');
|
||||
cleanupIntervals.forEach((interval) => clearInterval(interval));
|
||||
cleanupIntervals.clear();
|
||||
await clearAllExpiredFromCache();
|
||||
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Final cleanup completed');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keyv cache specified by type.
|
||||
* If an invalid type is passed, an error will be thrown.
|
||||
*
|
||||
* @param {string} key - The key for the namespace to access
|
||||
* @returns {Keyv} - If a valid key is passed, returns an object containing the cache store of the specified key.
|
||||
* @throws Will throw an error if an invalid key is passed.
|
||||
*/
|
||||
const getLogStores = (key) => {
|
||||
if (!key || !namespaces[key]) {
|
||||
throw new Error(`Invalid store key: ${key}`);
|
||||
}
|
||||
return namespaces[key];
|
||||
};
|
||||
|
||||
module.exports = getLogStores;
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
const getLogStores = require('./getLogStores');
|
||||
const logViolation = require('./logViolation');
|
||||
|
||||
module.exports = { getLogStores, logViolation };
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const getLogStores = require('./getLogStores');
|
||||
const banViolation = require('./banViolation');
|
||||
|
||||
/**
|
||||
* Logs the violation.
|
||||
*
|
||||
* @param {Object} req - Express request object containing user information.
|
||||
* @param {Object} res - Express response object.
|
||||
* @param {string} type - The type of violation.
|
||||
* @param {Object} errorMessage - The error message to log.
|
||||
* @param {number | string} [score=1] - The severity of the violation. Defaults to 1
|
||||
*/
|
||||
const logViolation = async (req, res, type, errorMessage, score = 1) => {
|
||||
const userId = req.user?.id ?? req.user?._id;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const logs = getLogStores(ViolationTypes.GENERAL);
|
||||
const violationLogs = getLogStores(type);
|
||||
const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId;
|
||||
|
||||
const userViolations = (await violationLogs.get(key)) ?? 0;
|
||||
const violationCount = +userViolations + +score;
|
||||
await violationLogs.set(key, violationCount);
|
||||
|
||||
errorMessage.user_id = userId;
|
||||
errorMessage.prev_count = userViolations;
|
||||
errorMessage.violation_count = violationCount;
|
||||
errorMessage.date = new Date().toISOString();
|
||||
|
||||
await banViolation(req, res, errorMessage);
|
||||
const userLogs = (await logs.get(key)) ?? [];
|
||||
userLogs.push(errorMessage);
|
||||
delete errorMessage.user_id;
|
||||
await logs.set(key, userLogs);
|
||||
};
|
||||
|
||||
module.exports = logViolation;
|
||||
Reference in New Issue
Block a user